Merge "Fix FilterClientCallbackImpl crash caused by race condition" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 705a4df..c231b30 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -57,7 +57,7 @@
     ":android.app.flags-aconfig-java{.generated_srcjars}",
     ":android.credentials.flags-aconfig-java{.generated_srcjars}",
     ":android.view.contentprotection.flags-aconfig-java{.generated_srcjars}",
-    ":com.android.server.flags.pinner-aconfig-java{.generated_srcjars}",
+    ":com.android.server.flags.services-aconfig-java{.generated_srcjars}",
     ":android.service.controls.flags-aconfig-java{.generated_srcjars}",
     ":android.service.voice.flags-aconfig-java{.generated_srcjars}",
     ":android.media.tv.flags-aconfig-java{.generated_srcjars}",
@@ -430,10 +430,7 @@
 aconfig_declarations {
     name: "com.android.media.flags.bettertogether-aconfig",
     package: "com.android.media.flags",
-    srcs: [
-        "media/java/android/media/flags/media_better_together.aconfig",
-        "media/java/android/media/flags/fade_manager_configuration.aconfig",
-    ],
+    srcs: ["media/java/android/media/flags/media_better_together.aconfig"],
 }
 
 java_aconfig_library {
@@ -591,16 +588,16 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
-// Pinner Service
+// Server Services Flags
 aconfig_declarations {
-    name: "com.android.server.flags.pinner-aconfig",
+    name: "com.android.server.flags.services-aconfig",
     package: "com.android.server.flags",
-    srcs: ["services/core/java/com/android/server/flags/pinner.aconfig"],
+    srcs: ["services/core/java/com/android/server/flags/*.aconfig"],
 }
 
 java_aconfig_library {
-    name: "com.android.server.flags.pinner-aconfig-java",
-    aconfig_declarations: "com.android.server.flags.pinner-aconfig",
+    name: "com.android.server.flags.services-aconfig-java",
+    aconfig_declarations: "com.android.server.flags.services-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
diff --git a/Android.bp b/Android.bp
index 96d26cf..13b1703 100644
--- a/Android.bp
+++ b/Android.bp
@@ -97,6 +97,7 @@
         // AIDL sources from external directories
         ":android.hardware.biometrics.common-V4-java-source",
         ":android.hardware.biometrics.fingerprint-V3-java-source",
+        ":android.hardware.biometrics.face-V4-java-source",
         ":android.hardware.gnss-V2-java-source",
         ":android.hardware.graphics.common-V3-java-source",
         ":android.hardware.keymaster-V4-java-source",
@@ -173,6 +174,9 @@
         // and remove this line.
         "//frameworks/base/tools/hoststubgen:__subpackages__",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // AIDL files under these paths are mixture of public and private ones.
@@ -206,11 +210,13 @@
 // framework_srcs. These have no or very limited dependency to the framework.
 java_library {
     name: "framework-internal-utils",
+    defaults: [
+        "android.hardware.power-java_static",
+    ],
     static_libs: [
         "apex_aidl_interface-java",
         "packagemanager_aidl-java",
         "framework-protos",
-        "libtombstone_proto_java",
         "updatable-driver-protos",
         "ota_metadata_proto_java",
         "android.hidl.base-V1.0-java",
@@ -261,6 +267,9 @@
     ],
     sdk_version: "core_platform",
     installable: false,
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // NOTE: This filegroup is exposed for vendor libraries to depend on and is referenced in
@@ -429,6 +438,9 @@
     ],
     sdk_version: "core_platform",
     installable: false,
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Separated so framework-minus-apex-defaults can be used without the libs dependency
@@ -472,6 +484,9 @@
     ],
     compile_dex: false,
     headers_only: true,
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -499,6 +514,9 @@
             "-Xep:AndroidFrameworkUid:ERROR",
         ],
     },
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -513,6 +531,7 @@
     },
     lint: {
         enabled: false,
+        baseline_filename: "lint-baseline.xml",
     },
 }
 
@@ -537,6 +556,9 @@
     ],
     sdk_version: "core_platform",
     apex_available: ["//apex_available:platform"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -552,6 +574,9 @@
         "calendar-provider-compat-config",
         "contacts-provider-platform-compat-config",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 platform_compat_config {
@@ -606,6 +631,9 @@
         "rappor",
     ],
     dxflags: ["--core-library"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // utility classes statically linked into framework-wifi and dynamically linked
@@ -631,6 +659,9 @@
         "//frameworks/base/services/net",
         "//packages/modules/Wifi/framework",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 filegroup {
diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp
index d03bbd2..e7adf20 100644
--- a/ProtoLibraries.bp
+++ b/ProtoLibraries.bp
@@ -34,6 +34,7 @@
         ":ipconnectivity-proto-src",
         ":libstats_atom_enum_protos",
         ":libstats_atom_message_protos",
+        ":libtombstone_proto-src",
         "core/proto/**/*.proto",
         "libs/incident/**/*.proto",
     ],
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 03f3f0f..f330ad1 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -65,7 +65,7 @@
 // depend on it.
 java_genrule {
     name: "framework-minus-apex.ravenwood",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
     cmd: "cp $(in) $(out)",
     srcs: [
         ":framework-minus-apex.ravenwood-base{ravenwood.jar}",
diff --git a/SECURITY_STATE_OWNERS b/SECURITY_STATE_OWNERS
new file mode 100644
index 0000000..30ddfe2
--- /dev/null
+++ b/SECURITY_STATE_OWNERS
@@ -0,0 +1,5 @@
+alxu@google.com
+musashi@google.com
+maunik@google.com
+davidkwak@google.com
+willcoster@google.com
\ No newline at end of file
diff --git a/STABILITY_OWNERS b/STABILITY_OWNERS
deleted file mode 100644
index a7ecb4d..0000000
--- a/STABILITY_OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-gaillard@google.com
-
diff --git a/apct-tests/perftests/healthconnect/Android.bp b/apct-tests/perftests/healthconnect/Android.bp
new file mode 100644
index 0000000..c2d0a6f
--- /dev/null
+++ b/apct-tests/perftests/healthconnect/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+    // 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: "HealthConnectPerfTests",
+
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "apct-perftests-utils",
+        "collector-device-lib-platform",
+    ],
+
+    libs: ["android.test.base"],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+    data: [":perfetto_artifacts"],
+    certificate: "platform",
+}
diff --git a/apct-tests/perftests/healthconnect/AndroidManifest.xml b/apct-tests/perftests/healthconnect/AndroidManifest.xml
new file mode 100644
index 0000000..6a6370b
--- /dev/null
+++ b/apct-tests/perftests/healthconnect/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.perftests.healthconnect">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.perftests.healthconnect"/>
+</manifest>
diff --git a/apct-tests/perftests/healthconnect/AndroidTest.xml b/apct-tests/perftests/healthconnect/AndroidTest.xml
new file mode 100644
index 0000000..5036202
--- /dev/null
+++ b/apct-tests/perftests/healthconnect/AndroidTest.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs HealthConnectPerfTests metric instrumentation.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-metric-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="HealthConnectPerfTests.apk" />
+    </target_preparer>
+
+    <!-- Needed for pushing the trace config file -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+    </target_preparer>
+
+    <!-- Needed for pulling the collected trace config on to the host -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path" />
+    </metrics_collector>
+
+    <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+    <option name="isolated-storage" value="false" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.perftests.healthconnect" />
+        <option name="hidden-api-checks" value="false"/>
+
+         <!-- Listener related args for collecting the traces and waiting for the device to stabilize. -->
+        <option name="device-listeners" value="android.device.collectors.ProcLoadListener,android.device.collectors.PerfettoListener" />
+        <!-- Guarantee that user defined RunListeners will be running before any of the default listeners defined in this runner. -->
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+
+        <!-- ProcLoadListener related arguments -->
+        <!-- Wait for device last minute threshold to reach 3 with 2 minute timeout before starting the test run -->
+        <option name="instrumentation-arg" key="procload-collector:per_run" value="true" />
+        <option name="instrumentation-arg" key="proc-loadavg-threshold" value="3" />
+        <option name="instrumentation-arg" key="proc-loadavg-timeout" value="120000" />
+        <option name="instrumentation-arg" key="proc-loadavg-interval" value="10000" />
+
+        <!-- PerfettoListener related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+    </test>
+</configuration>
diff --git a/apct-tests/perftests/healthconnect/OWNERS b/apct-tests/perftests/healthconnect/OWNERS
new file mode 100644
index 0000000..da0b46a
--- /dev/null
+++ b/apct-tests/perftests/healthconnect/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 1219472
+
+arkivanov@google.com
+jstembridge@google.com
+pratyushmore@google.com
+itsleo@google.com
diff --git a/apct-tests/perftests/healthconnect/src/com/android/perftests/healthconnect/HealthConnectReadWritePerfTest.kt b/apct-tests/perftests/healthconnect/src/com/android/perftests/healthconnect/HealthConnectReadWritePerfTest.kt
new file mode 100644
index 0000000..21a4ca0
--- /dev/null
+++ b/apct-tests/perftests/healthconnect/src/com/android/perftests/healthconnect/HealthConnectReadWritePerfTest.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.perftests.healthconnect
+
+import android.health.connect.HealthConnectManager
+import android.os.SystemClock
+import android.perftests.utils.PerfStatusReporter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Read/write benchmark tests for [HealthConnectManager]
+ *
+ * Build/Install/Run: atest HealthConnectReadWritePerfTest
+ */
+@RunWith(AndroidJUnit4::class)
+class HealthConnectReadWritePerfTest {
+
+    @get:Rule
+    val perfStatusReporter = PerfStatusReporter()
+
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+
+    private val manager by lazy {
+        requireNotNull(context.getSystemService(HealthConnectManager::class.java))
+    }
+
+    /**
+     * A first empty test just to setup the test package and make sure it runs properly.
+     */
+    @Test
+    fun placeholder() {
+        val state = perfStatusReporter.benchmarkState
+        while (state.keepRunning()) {
+            SystemClock.sleep(100)
+        }
+    }
+}
\ No newline at end of file
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 83db4cb..d940e38 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1828,7 +1828,14 @@
                     /* system_measured_source_download_bytes */0,
                     /* system_measured_source_upload_bytes */ 0,
                     /* system_measured_calling_download_bytes */0,
-                    /* system_measured_calling_upload_bytes */ 0);
+                    /* system_measured_calling_upload_bytes */ 0,
+                    jobStatus.getJob().getIntervalMillis(),
+                    jobStatus.getJob().getFlexMillis(),
+                    jobStatus.hasFlexibilityConstraint(),
+                    /* isFlexConstraintSatisfied */ false,
+                    jobStatus.canApplyTransportAffinities(),
+                    jobStatus.getNumAppliedFlexibleConstraints(),
+                    jobStatus.getNumDroppedFlexibleConstraints());
 
             // If the job is immediately ready to run, then we can just immediately
             // put it in the pending list and try to schedule it.  This is especially
@@ -2269,7 +2276,14 @@
                     /* system_measured_source_download_bytes */ 0,
                     /* system_measured_source_upload_bytes */ 0,
                     /* system_measured_calling_download_bytes */0,
-                    /* system_measured_calling_upload_bytes */ 0);
+                    /* system_measured_calling_upload_bytes */ 0,
+                    cancelled.getJob().getIntervalMillis(),
+                    cancelled.getJob().getFlexMillis(),
+                    cancelled.hasFlexibilityConstraint(),
+                    cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE),
+                    cancelled.canApplyTransportAffinities(),
+                    cancelled.getNumAppliedFlexibleConstraints(),
+                    cancelled.getNumDroppedFlexibleConstraints());
         }
         // If this is a replacement, bring in the new version of the job
         if (incomingJob != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 6449edc..fe55e27 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -534,7 +534,14 @@
                     /* system_measured_source_download_bytes */ 0,
                     /* system_measured_source_upload_bytes */ 0,
                     /* system_measured_calling_download_bytes */ 0,
-                    /* system_measured_calling_upload_bytes */ 0);
+                    /* system_measured_calling_upload_bytes */ 0,
+                    job.getJob().getIntervalMillis(),
+                    job.getJob().getFlexMillis(),
+                    job.hasFlexibilityConstraint(),
+                    job.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE),
+                    job.canApplyTransportAffinities(),
+                    job.getNumAppliedFlexibleConstraints(),
+                    job.getNumDroppedFlexibleConstraints());
             sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount());
             final String sourcePackage = job.getSourcePackageName();
             if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
@@ -1616,7 +1623,14 @@
                 TrafficStats.getUidRxBytes(completedJob.getUid())
                         - mInitialDownloadedBytesFromCalling,
                 TrafficStats.getUidTxBytes(completedJob.getUid())
-                        - mInitialUploadedBytesFromCalling);
+                        - mInitialUploadedBytesFromCalling,
+                completedJob.getJob().getIntervalMillis(),
+                completedJob.getJob().getFlexMillis(),
+                completedJob.hasFlexibilityConstraint(),
+                completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE),
+                completedJob.canApplyTransportAffinities(),
+                completedJob.getNumAppliedFlexibleConstraints(),
+                completedJob.getNumDroppedFlexibleConstraints());
         if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
                     getId());
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 0d5d11e..bdc2246 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -106,11 +106,8 @@
     public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
     public static final long NO_EARLIEST_RUNTIME = 0L;
 
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static final int CONSTRAINT_BATTERY_NOT_LOW =
             JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -125,7 +122,7 @@
     static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
     static final int CONSTRAINT_PREFETCH = 1 << 23;
     static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
-    static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint
+    public static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint
 
     private static final int IMPLICIT_CONSTRAINTS = 0
             | CONSTRAINT_BACKGROUND_NOT_RESTRICTED
@@ -1534,7 +1531,7 @@
 
     /**
      * Returns the number of required flexible job constraints that have been dropped with time.
-     * The lower this number is the easier it is for the flexibility constraint to be satisfied.
+     * The higher this number is the easier it is for the flexibility constraint to be satisfied.
      */
     public int getNumDroppedFlexibleConstraints() {
         return mNumDroppedFlexibleConstraints;
@@ -1600,7 +1597,8 @@
         mTransportAffinitiesSatisfied = isSatisfied;
     }
 
-    boolean canApplyTransportAffinities() {
+    /** Whether transport affinities can be applied to the job in flex scheduling. */
+    public boolean canApplyTransportAffinities() {
         return mCanApplyTransportAffinities;
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index 913a76a..4d4e340 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -591,6 +591,16 @@
         if (idle) {
             newBucket = IDLE_BUCKET_CUTOFF;
             reason = REASON_MAIN_FORCED_BY_USER;
+            final AppUsageHistory appHistory = getAppUsageHistory(packageName, userId,
+                    elapsedRealtime);
+            // Wipe all expiry times that could raise the bucket on reevaluation.
+            if (appHistory.bucketExpiryTimesMs != null) {
+                for (int i = appHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) {
+                    if (appHistory.bucketExpiryTimesMs.keyAt(i) < newBucket) {
+                        appHistory.bucketExpiryTimesMs.removeAt(i);
+                    }
+                }
+            }
         } else {
             newBucket = STANDBY_BUCKET_ACTIVE;
             // This is to pretend that the app was just used, don't freeze the state anymore.
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index bd5c006..e589c21 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -59,6 +59,7 @@
 import static com.android.server.usage.AppIdleHistory.STANDBY_BUCKET_UNKNOWN;
 
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -2146,6 +2147,15 @@
         }
     }
 
+    /**
+     * Flush the handler.
+     * Returns true if successfully flushed within the timeout, otherwise return false.
+     */
+    @VisibleForTesting
+    boolean flushHandler(@DurationMillisLong long timeoutMillis) {
+        return mHandler.runWithScissors(() -> {}, timeoutMillis);
+    }
+
     @Override
     public void flushToDisk() {
         synchronized (mAppIdleLock) {
diff --git a/api/Android.bp b/api/Android.bp
index 9029d25..363197a 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -307,10 +307,6 @@
         "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",
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 820d2b0..77b74e9 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -398,8 +398,8 @@
             break;
     }
 
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
diff --git a/core/api/current.txt b/core/api/current.txt
index d1d7983..ec8f070 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9315,10 +9315,14 @@
   public final class StorageStats implements android.os.Parcelable {
     method public int describeContents();
     method public long getAppBytes();
+    method @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public long getAppBytesByDataType(int);
     method public long getCacheBytes();
     method public long getDataBytes();
     method public long getExternalCacheBytes();
     method public void writeToParcel(android.os.Parcel, int);
+    field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_APK = 0; // 0x0
+    field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_FILE_TYPE_DM = 1; // 0x1
+    field @FlaggedApi("android.app.usage.get_app_bytes_by_data_type_api") public static final int APP_DATA_TYPE_LIB = 2; // 0x2
     field @NonNull public static final android.os.Parcelable.Creator<android.app.usage.StorageStats> CREATOR;
   }
 
@@ -13418,12 +13422,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 @FlaggedApi("android.content.pm.archiving") public SigningInfo(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 @FlaggedApi("android.content.pm.archiving") public int getSchemeVersion();
     method public android.content.pm.Signature[] getSigningCertificateHistory();
     method public boolean hasMultipleSigners();
     method public boolean hasPastSigningCertificates();
@@ -13657,6 +13661,16 @@
     field public int uiMode;
   }
 
+  @FlaggedApi("android.content.res.font_scale_converter_public") public interface FontScaleConverter {
+    method public float convertDpToSp(float);
+    method public float convertSpToDp(float);
+  }
+
+  @FlaggedApi("android.content.res.font_scale_converter_public") public class FontScaleConverterFactory {
+    method @FlaggedApi("android.content.res.font_scale_converter_public") @AnyThread @Nullable public static android.content.res.FontScaleConverter forScale(float);
+    method @FlaggedApi("android.content.res.font_scale_converter_public") @AnyThread public static boolean isNonLinearFontScalingActive(float);
+  }
+
   public class ObbInfo implements android.os.Parcelable {
     method public int describeContents();
     method public void writeToParcel(android.os.Parcel, int);
@@ -23962,6 +23976,7 @@
     method @Nullable public android.net.Uri getIconUri();
     method @NonNull public String getId();
     method @NonNull public CharSequence getName();
+    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public int getSuitabilityStatus();
     method public int getType();
     method public int getVolume();
     method public int getVolumeHandling();
@@ -23979,6 +23994,9 @@
     field public static final String FEATURE_REMOTE_VIDEO_PLAYBACK = "android.media.route.feature.REMOTE_VIDEO_PLAYBACK";
     field public static final int PLAYBACK_VOLUME_FIXED = 0; // 0x0
     field public static final int PLAYBACK_VOLUME_VARIABLE = 1; // 0x1
+    field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER = 2; // 0x2
+    field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER = 0; // 0x0
+    field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER = 1; // 0x1
     field public static final int TYPE_BLE_HEADSET = 26; // 0x1a
     field public static final int TYPE_BLUETOOTH_A2DP = 8; // 0x8
     field public static final int TYPE_BUILTIN_SPEAKER = 2; // 0x2
@@ -24019,6 +24037,7 @@
     method @NonNull public android.media.MediaRoute2Info.Builder setDescription(@Nullable CharSequence);
     method @NonNull public android.media.MediaRoute2Info.Builder setExtras(@Nullable android.os.Bundle);
     method @NonNull public android.media.MediaRoute2Info.Builder setIconUri(@Nullable android.net.Uri);
+    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") @NonNull public android.media.MediaRoute2Info.Builder setSuitabilityStatus(int);
     method @NonNull public android.media.MediaRoute2Info.Builder setType(int);
     method @NonNull public android.media.MediaRoute2Info.Builder setVisibilityPublic();
     method @NonNull public android.media.MediaRoute2Info.Builder setVisibilityRestricted(@NonNull java.util.Set<java.lang.String>);
@@ -24232,6 +24251,7 @@
     method public void release();
     method public void selectRoute(@NonNull android.media.MediaRoute2Info);
     method public void setVolume(int);
+    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferRequestedBySelf();
   }
 
   public abstract static class MediaRouter2.TransferCallback {
@@ -24629,12 +24649,16 @@
     method @Nullable public CharSequence getName();
     method @NonNull public java.util.List<java.lang.String> getSelectableRoutes();
     method @NonNull public java.util.List<java.lang.String> getSelectedRoutes();
+    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public int getTransferReason();
     method @NonNull public java.util.List<java.lang.String> getTransferableRoutes();
     method public int getVolume();
     method public int getVolumeHandling();
     method public int getVolumeMax();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.media.RoutingSessionInfo> CREATOR;
+    field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int TRANSFER_REASON_APP = 2; // 0x2
+    field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int TRANSFER_REASON_FALLBACK = 0; // 0x0
+    field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1; // 0x1
   }
 
   public static final class RoutingSessionInfo.Builder {
@@ -24655,6 +24679,8 @@
     method @NonNull public android.media.RoutingSessionInfo.Builder removeTransferableRoute(@NonNull String);
     method @NonNull public android.media.RoutingSessionInfo.Builder setControlHints(@Nullable android.os.Bundle);
     method @NonNull public android.media.RoutingSessionInfo.Builder setName(@Nullable CharSequence);
+    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") @NonNull public android.media.RoutingSessionInfo.Builder setTransferInitiator(@Nullable android.os.UserHandle, @Nullable String);
+    method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") @NonNull public android.media.RoutingSessionInfo.Builder setTransferReason(int);
     method @NonNull public android.media.RoutingSessionInfo.Builder setVolume(int);
     method @NonNull public android.media.RoutingSessionInfo.Builder setVolumeHandling(int);
     method @NonNull public android.media.RoutingSessionInfo.Builder setVolumeMax(int);
@@ -35864,19 +35890,19 @@
     field public static final String NAMESPACE = "data2";
   }
 
-  public static final class ContactsContract.CommonDataKinds.Im implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
-    method public static CharSequence getProtocolLabel(android.content.res.Resources, int, CharSequence);
-    method public static int getProtocolLabelResource(int);
-    method public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
-    method public static int getTypeLabelResource(int);
-    field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im";
-    field public static final String CUSTOM_PROTOCOL = "data6";
+  @Deprecated public static final class ContactsContract.CommonDataKinds.Im implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
+    method @Deprecated public static CharSequence getProtocolLabel(android.content.res.Resources, int, CharSequence);
+    method @Deprecated public static int getProtocolLabelResource(int);
+    method @Deprecated public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
+    method @Deprecated public static int getTypeLabelResource(int);
+    field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im";
+    field @Deprecated public static final String CUSTOM_PROTOCOL = "data6";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
-    field public static final String PROTOCOL = "data5";
+    field @Deprecated public static final String PROTOCOL = "data5";
     field @Deprecated public static final int PROTOCOL_AIM = 0; // 0x0
-    field public static final int PROTOCOL_CUSTOM = -1; // 0xffffffff
+    field @Deprecated public static final int PROTOCOL_CUSTOM = -1; // 0xffffffff
     field @Deprecated public static final int PROTOCOL_GOOGLE_TALK = 5; // 0x5
     field @Deprecated public static final int PROTOCOL_ICQ = 6; // 0x6
     field @Deprecated public static final int PROTOCOL_JABBER = 7; // 0x7
@@ -35885,9 +35911,9 @@
     field @Deprecated public static final int PROTOCOL_QQ = 4; // 0x4
     field @Deprecated public static final int PROTOCOL_SKYPE = 3; // 0x3
     field @Deprecated public static final int PROTOCOL_YAHOO = 2; // 0x2
-    field public static final int TYPE_HOME = 1; // 0x1
-    field public static final int TYPE_OTHER = 3; // 0x3
-    field public static final int TYPE_WORK = 2; // 0x2
+    field @Deprecated public static final int TYPE_HOME = 1; // 0x1
+    field @Deprecated public static final int TYPE_OTHER = 3; // 0x3
+    field @Deprecated public static final int TYPE_WORK = 2; // 0x2
   }
 
   public static final class ContactsContract.CommonDataKinds.Nickname implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
@@ -36002,17 +36028,17 @@
     field public static final int TYPE_SPOUSE = 14; // 0xe
   }
 
-  public static final class ContactsContract.CommonDataKinds.SipAddress implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
-    method public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
-    method public static int getTypeLabelResource(int);
-    field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sip_address";
+  @Deprecated public static final class ContactsContract.CommonDataKinds.SipAddress implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
+    method @Deprecated public static CharSequence getTypeLabel(android.content.res.Resources, int, @Nullable CharSequence);
+    method @Deprecated public static int getTypeLabelResource(int);
+    field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sip_address";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
-    field public static final String SIP_ADDRESS = "data1";
-    field public static final int TYPE_HOME = 1; // 0x1
-    field public static final int TYPE_OTHER = 3; // 0x3
-    field public static final int TYPE_WORK = 2; // 0x2
+    field @Deprecated public static final String SIP_ADDRESS = "data1";
+    field @Deprecated public static final int TYPE_HOME = 1; // 0x1
+    field @Deprecated public static final int TYPE_OTHER = 3; // 0x3
+    field @Deprecated public static final int TYPE_WORK = 2; // 0x2
   }
 
   public static final class ContactsContract.CommonDataKinds.StructuredName implements android.provider.ContactsContract.DataColumnsWithJoins {
@@ -42791,6 +42817,7 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.ANSWER_PHONE_CALLS) public boolean endCall();
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.net.Uri getAdnUriForPhoneAccount(android.telecom.PhoneAccountHandle);
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telecom.PhoneAccountHandle> getCallCapablePhoneAccounts();
+    method @FlaggedApi("com.android.internal.telephony.flags.work_profile_api_split") @NonNull @RequiresPermission(allOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_PROFILES}) public java.util.List<android.telecom.PhoneAccountHandle> getCallCapablePhoneAccountsAcrossProfiles();
     method public String getDefaultDialerPackage();
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telecom.PhoneAccountHandle getDefaultOutgoingPhoneAccount(String);
     method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_SMS, android.Manifest.permission.READ_PHONE_NUMBERS}, conditional=true) public String getLine1Number(android.telecom.PhoneAccountHandle);
@@ -43266,6 +43293,7 @@
     field public static final String KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY = "cdma_nonroaming_networks_string_array";
     field public static final String KEY_CDMA_ROAMING_MODE_INT = "cdma_roaming_mode_int";
     field public static final String KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY = "cdma_roaming_networks_string_array";
+    field @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") public static final String KEY_CELLULAR_SERVICE_CAPABILITIES_INT_ARRAY = "cellular_service_capabilities_int_array";
     field public static final String KEY_CELLULAR_USAGE_SETTING_INT = "cellular_usage_setting_int";
     field public static final String KEY_CHECK_PRICING_WITH_CARRIER_FOR_DATA_ROAMING_BOOL = "check_pricing_with_carrier_data_roaming_bool";
     field public static final String KEY_CI_ACTION_ON_SYS_UPDATE_BOOL = "ci_action_on_sys_update_bool";
@@ -43392,6 +43420,7 @@
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT = "parameters_used_for_ntn_lte_signal_bar_int";
     field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool";
     field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.hide_prefer_3g_item") public static final String KEY_PREFER_3G_VISIBILITY_BOOL = "prefer_3g_visibility_bool";
     field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_daily_notification_count_int";
     field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_monthly_notification_count_int";
     field public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = "premium_capability_network_setup_time_millis_long";
@@ -45260,6 +45289,7 @@
     method @Nullable public String getMncString();
     method @Deprecated public String getNumber();
     method public int getPortIndex();
+    method @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") @NonNull public java.util.Set<java.lang.Integer> getServiceCapabilities();
     method public int getSimSlotIndex();
     method public int getSubscriptionId();
     method public int getSubscriptionType();
@@ -45341,6 +45371,9 @@
     field public static final int PHONE_NUMBER_SOURCE_CARRIER = 2; // 0x2
     field public static final int PHONE_NUMBER_SOURCE_IMS = 3; // 0x3
     field public static final int PHONE_NUMBER_SOURCE_UICC = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") public static final int SERVICE_CAPABILITY_DATA = 3; // 0x3
+    field @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") public static final int SERVICE_CAPABILITY_SMS = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") public static final int SERVICE_CAPABILITY_VOICE = 1; // 0x1
     field public static final int SUBSCRIPTION_TYPE_LOCAL_SIM = 0; // 0x0
     field public static final int SUBSCRIPTION_TYPE_REMOTE_SIM = 1; // 0x1
     field public static final int USAGE_SETTING_DATA_CENTRIC = 2; // 0x2
@@ -45589,6 +45622,8 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_BASIC_PHONE_STATE}) public boolean isDataEnabled();
     method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.READ_BASIC_PHONE_STATE}) public boolean isDataEnabledForReason(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_BASIC_PHONE_STATE}) public boolean isDataRoamingEnabled();
+    method @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") public boolean isDeviceSmsCapable();
+    method @FlaggedApi("com.android.internal.telephony.flags.data_only_cellular_service") public boolean isDeviceVoiceCapable();
     method public boolean isEmergencyNumber(@NonNull String);
     method public boolean isHearingAidCompatibilitySupported();
     method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRECISE_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE"}) public boolean isManualNetworkSelectionAllowed();
@@ -45598,9 +45633,9 @@
     method @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE) public boolean isPremiumCapabilityAvailableForPurchase(int);
     method public boolean isRadioInterfaceCapabilitySupported(@NonNull String);
     method public boolean isRttSupported();
-    method public boolean isSmsCapable();
+    method @Deprecated public boolean isSmsCapable();
     method @Deprecated public boolean isTtyModeSupported();
-    method public boolean isVoiceCapable();
+    method @Deprecated public boolean isVoiceCapable();
     method public boolean isVoicemailVibrationEnabled(android.telecom.PhoneAccountHandle);
     method public boolean isWorldPhone();
     method @Deprecated public void listen(android.telephony.PhoneStateListener, int);
@@ -45647,6 +45682,7 @@
     field public static final String ACTION_MULTI_SIM_CONFIG_CHANGED = "android.telephony.action.MULTI_SIM_CONFIG_CHANGED";
     field public static final String ACTION_NETWORK_COUNTRY_CHANGED = "android.telephony.action.NETWORK_COUNTRY_CHANGED";
     field @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public static final String ACTION_PHONE_STATE_CHANGED = "android.intent.action.PHONE_STATE";
+    field @FlaggedApi("com.android.internal.telephony.flags.reset_mobile_network_settings") public static final String ACTION_RESET_MOBILE_NETWORK_SETTINGS = "android.telephony.action.RESET_MOBILE_NETWORK_SETTINGS";
     field public static final String ACTION_RESPOND_VIA_MESSAGE = "android.intent.action.RESPOND_VIA_MESSAGE";
     field public static final String ACTION_SECRET_CODE = "android.telephony.action.SECRET_CODE";
     field public static final String ACTION_SHOW_VOICEMAIL_NOTIFICATION = "android.telephony.action.SHOW_VOICEMAIL_NOTIFICATION";
@@ -46612,6 +46648,8 @@
     method public int getTransportType();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field public static final int ATTR_EPDG_OVER_CELL_INTERNET = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") public static final int ATTR_REGISTRATION_TYPE_EMERGENCY = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") public static final int ATTR_VIRTUAL_FOR_ANONYMOUS_EMERGENCY_CALL = 4; // 0x4
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.ImsRegistrationAttributes> CREATOR;
   }
 
@@ -53511,6 +53549,7 @@
     method public android.transition.Transition getExitTransition();
     method protected final int getFeatures();
     method protected final int getForcedWindowFlags();
+    method @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public boolean getFrameRateBoostOnTouchEnabled();
     method @Nullable public android.view.WindowInsetsController getInsetsController();
     method @NonNull public abstract android.view.LayoutInflater getLayoutInflater();
     method protected final int getLocalFeatures();
@@ -53588,6 +53627,7 @@
     method public abstract void setFeatureInt(int, int);
     method public void setFlags(int, int);
     method public void setFormat(int);
+    method @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public void setFrameRateBoostOnTouchEnabled(boolean);
     method public void setGravity(int);
     method @RequiresPermission(android.Manifest.permission.HIDE_OVERLAY_WINDOWS) public final void setHideOverlayWindows(boolean);
     method public void setIcon(@DrawableRes int);
@@ -53897,6 +53937,7 @@
     field public static final String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH";
     field public static final String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE";
     field public static final String PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE";
+    field @FlaggedApi("com.android.window.flags.app_compat_properties_api") public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
     field public static final String PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE";
     field public static final String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS";
     field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS";
@@ -53931,6 +53972,7 @@
     method @FlaggedApi("com.android.graphics.hwui.flags.limited_hdr") public float getDesiredHdrHeadroom();
     method public int getFitInsetsSides();
     method public int getFitInsetsTypes();
+    method @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public boolean getFrameRateBoostOnTouchEnabled();
     method public final CharSequence getTitle();
     method public boolean isFitInsetsIgnoringVisibility();
     method public boolean isHdrConversionEnabled();
@@ -53942,6 +53984,7 @@
     method public void setFitInsetsIgnoringVisibility(boolean);
     method public void setFitInsetsSides(int);
     method public void setFitInsetsTypes(int);
+    method @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public void setFrameRateBoostOnTouchEnabled(boolean);
     method public void setHdrConversionEnabled(boolean);
     method public final void setTitle(CharSequence);
     method public void setWallpaperTouchEventsEnabled(boolean);
@@ -57369,7 +57412,7 @@
     method public abstract boolean getBuiltInZoomControls();
     method public abstract int getCacheMode();
     method public abstract String getCursiveFontFamily();
-    method public abstract boolean getDatabaseEnabled();
+    method @Deprecated public abstract boolean getDatabaseEnabled();
     method @Deprecated public abstract String getDatabasePath();
     method public abstract int getDefaultFixedFontSize();
     method public abstract int getDefaultFontSize();
@@ -57415,7 +57458,7 @@
     method public abstract void setBuiltInZoomControls(boolean);
     method public abstract void setCacheMode(int);
     method public abstract void setCursiveFontFamily(String);
-    method public abstract void setDatabaseEnabled(boolean);
+    method @Deprecated public abstract void setDatabaseEnabled(boolean);
     method @Deprecated public abstract void setDatabasePath(String);
     method public abstract void setDefaultFixedFontSize(int);
     method public abstract void setDefaultFontSize(int);
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index d395b8c..de330de 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -6,7 +6,6 @@
     field public static final String CONTROL_AUTOMOTIVE_GNSS = "android.permission.CONTROL_AUTOMOTIVE_GNSS";
     field public static final String GET_INTENT_SENDER_INTENT = "android.permission.GET_INTENT_SENDER_INTENT";
     field public static final String MAKE_UID_VISIBLE = "android.permission.MAKE_UID_VISIBLE";
-    field @FlaggedApi("android.companion.flags.companion_transport_apis") public static final String USE_COMPANION_TRANSPORTS = "android.permission.USE_COMPANION_TRANSPORTS";
   }
 
 }
@@ -82,29 +81,6 @@
 
 }
 
-package android.companion {
-
-  public final class CompanionDeviceManager {
-    method @FlaggedApi("android.companion.companion_transport_apis") @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void addOnMessageReceivedListener(@NonNull java.util.concurrent.Executor, int, @NonNull android.companion.CompanionDeviceManager.OnMessageReceivedListener);
-    method @FlaggedApi("android.companion.companion_transport_apis") @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void addOnTransportsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.CompanionDeviceManager.OnTransportsChangedListener);
-    method @FlaggedApi("android.companion.companion_transport_apis") @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void removeOnMessageReceivedListener(int, @NonNull android.companion.CompanionDeviceManager.OnMessageReceivedListener);
-    method @FlaggedApi("android.companion.companion_transport_apis") @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void removeOnTransportsChangedListener(@NonNull android.companion.CompanionDeviceManager.OnTransportsChangedListener);
-    method @FlaggedApi("android.companion.companion_transport_apis") @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void sendMessage(int, @NonNull byte[], @NonNull int[]);
-    field @FlaggedApi("android.companion.companion_transport_apis") public static final int MESSAGE_REQUEST_CONTEXT_SYNC = 1667729539; // 0x63678883
-    field @FlaggedApi("android.companion.companion_transport_apis") public static final int MESSAGE_REQUEST_PERMISSION_RESTORE = 1669491075; // 0x63826983
-    field @FlaggedApi("android.companion.companion_transport_apis") public static final int MESSAGE_REQUEST_REMOTE_AUTHENTICATION = 1669494629; // 0x63827765
-  }
-
-  @FlaggedApi("android.companion.companion_transport_apis") public static interface CompanionDeviceManager.OnMessageReceivedListener {
-    method public void onMessageReceived(int, @NonNull byte[]);
-  }
-
-  @FlaggedApi("android.companion.companion_transport_apis") public static interface CompanionDeviceManager.OnTransportsChangedListener {
-    method public void onTransportsChanged(@NonNull java.util.List<android.companion.AssociationInfo>);
-  }
-
-}
-
 package android.content {
 
   public abstract class ContentProvider implements android.content.ComponentCallbacks2 {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fc0fb35..b15bc0e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -158,6 +158,7 @@
     field public static final String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL";
     field public static final String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW";
     field public static final String INVOKE_CARRIER_SETUP = "android.permission.INVOKE_CARRIER_SETUP";
+    field public static final String KEYPHRASE_ENROLLMENT_APPLICATION = "android.permission.KEYPHRASE_ENROLLMENT_APPLICATION";
     field public static final String KILL_ALL_BACKGROUND_PROCESSES = "android.permission.KILL_ALL_BACKGROUND_PROCESSES";
     field public static final String KILL_UID = "android.permission.KILL_UID";
     field public static final String LAUNCH_DEVICE_MANAGER_SETUP = "android.permission.LAUNCH_DEVICE_MANAGER_SETUP";
@@ -214,6 +215,7 @@
     field public static final String MANAGE_USB = "android.permission.MANAGE_USB";
     field public static final String MANAGE_USERS = "android.permission.MANAGE_USERS";
     field public static final String MANAGE_USER_OEM_UNLOCK_STATE = "android.permission.MANAGE_USER_OEM_UNLOCK_STATE";
+    field public static final String MANAGE_VOICE_KEYPHRASES = "android.permission.MANAGE_VOICE_KEYPHRASES";
     field public static final String MANAGE_WALLPAPER_EFFECTS_GENERATION = "android.permission.MANAGE_WALLPAPER_EFFECTS_GENERATION";
     field public static final String MANAGE_WEAK_ESCROW_TOKEN = "android.permission.MANAGE_WEAK_ESCROW_TOKEN";
     field public static final String MANAGE_WEARABLE_SENSING_SERVICE = "android.permission.MANAGE_WEARABLE_SENSING_SERVICE";
@@ -3348,14 +3350,14 @@
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
-    method public void onProcessCaptureRequest(int, long, @Nullable android.companion.virtual.camera.VirtualCameraMetadata);
+    method public default void onProcessCaptureRequest(int, long);
     method public void onStreamClosed(int);
     method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
     method public int describeContents();
-    method @StringRes public int getDisplayNameStringRes();
+    method @NonNull public String getName();
     method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR;
@@ -3365,16 +3367,10 @@
     ctor public VirtualCameraConfig.Builder();
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
-    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setDisplayNameStringRes(@StringRes int);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
   }
 
-  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraMetadata implements android.os.Parcelable {
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraMetadata> CREATOR;
-  }
-
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
     ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
     method public int describeContents();
@@ -6476,6 +6472,7 @@
     method public void clearAudioServerStateCallback();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean clearPreferredDevicesForCapturePreset(int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int dispatchAudioFocusChangeWithFade(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy, @NonNull java.util.List<android.media.AudioFocusInfo>, @Nullable android.media.FadeManagerConfiguration);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getActiveAssistantServicesUids();
     method @IntRange(from=0) public long getAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getAssistantServicesUids();
@@ -6675,6 +6672,69 @@
     field public static final int CONTENT_ID_NONE = 0; // 0x0
   }
 
+  @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") public final class FadeManagerConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.List<android.media.AudioAttributes> getAudioAttributesWithVolumeShaperConfigs();
+    method public long getFadeInDelayForOffenders();
+    method public long getFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method public long getFadeInDurationForUsage(int);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeInVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(int);
+    method public long getFadeOutDurationForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method public long getFadeOutDurationForUsage(int);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeOutVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @Nullable public android.media.VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(int);
+    method public int getFadeState();
+    method @NonNull public java.util.List<java.lang.Integer> getFadeableUsages();
+    method @NonNull public java.util.List<android.media.AudioAttributes> getUnfadeableAudioAttributes();
+    method @NonNull public java.util.List<java.lang.Integer> getUnfadeableContentTypes();
+    method @NonNull public java.util.List<java.lang.Integer> getUnfadeablePlayerTypes();
+    method @NonNull public java.util.List<java.lang.Integer> getUnfadeableUids();
+    method public boolean isAudioAttributesUnfadeable(@NonNull android.media.AudioAttributes);
+    method public boolean isContentTypeUnfadeable(int);
+    method public boolean isFadeEnabled();
+    method public boolean isPlayerTypeUnfadeable(int);
+    method public boolean isUidUnfadeable(int);
+    method public boolean isUsageFadeable(int);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.FadeManagerConfiguration> CREATOR;
+    field public static final long DURATION_NOT_SET = 0L; // 0x0L
+    field public static final int FADE_STATE_DISABLED = 0; // 0x0
+    field public static final int FADE_STATE_ENABLED_AUTO = 2; // 0x2
+    field public static final int FADE_STATE_ENABLED_DEFAULT = 1; // 0x1
+    field public static final String TAG = "FadeManagerConfiguration";
+    field public static final int VOLUME_SHAPER_SYSTEM_FADE_ID = 2; // 0x2
+  }
+
+  public static final class FadeManagerConfiguration.Builder {
+    ctor public FadeManagerConfiguration.Builder();
+    ctor public FadeManagerConfiguration.Builder(long, long);
+    ctor public FadeManagerConfiguration.Builder(@NonNull android.media.FadeManagerConfiguration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addFadeableUsage(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableContentType(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder addUnfadeableUid(int);
+    method @NonNull public android.media.FadeManagerConfiguration build();
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearFadeableUsage(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableAudioAttributes(@NonNull android.media.AudioAttributes);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableContentType(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder clearUnfadeableUid(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDelayForOffenders(long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInDurationForUsage(int, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeInVolumeShaperConfigForUsage(int, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutDurationForAudioAttributes(@NonNull android.media.AudioAttributes, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutDurationForUsage(int, long);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutVolumeShaperConfigForAudioAttributes(@NonNull android.media.AudioAttributes, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeOutVolumeShaperConfigForUsage(int, @Nullable android.media.VolumeShaper.Configuration);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeState(int);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setFadeableUsages(@NonNull java.util.List<java.lang.Integer>);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableAudioAttributes(@NonNull java.util.List<android.media.AudioAttributes>);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableContentTypes(@NonNull java.util.List<java.lang.Integer>);
+    method @NonNull public android.media.FadeManagerConfiguration.Builder setUnfadeableUids(@NonNull java.util.List<java.lang.Integer>);
+  }
+
   public class HwAudioSource {
     method public boolean isPlaying();
     method public void start();
@@ -6893,15 +6953,18 @@
 
   public class AudioPolicy {
     method public int attachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>);
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int clearFadeManagerConfigurationForFocusLoss();
     method public android.media.AudioRecord createAudioRecordSink(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
     method public android.media.AudioTrack createAudioTrackSource(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
     method public int detachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>);
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public android.media.FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss();
     method public int getFocusDuckingBehavior();
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioFocusInfo> getFocusStack();
     method public int getStatus();
     method public boolean removeUidDeviceAffinity(int);
     method public boolean removeUserIdDeviceAffinity(int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull android.media.AudioFocusInfo) throws java.lang.IllegalStateException;
+    method @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public int setFadeManagerConfigurationForFocusLoss(@NonNull android.media.FadeManagerConfiguration);
     method public int setFocusDuckingBehavior(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setRegistration(String);
     method public boolean setUidDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>);
@@ -8987,9 +9050,9 @@
 package android.media.voice {
 
   public final class KeyphraseModelManager {
-    method @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public void deleteKeyphraseSoundModel(int, @NonNull java.util.Locale);
-    method @Nullable @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel getKeyphraseSoundModel(int, @NonNull java.util.Locale);
-    method @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public void updateKeyphraseSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_VOICE_KEYPHRASES) public void deleteKeyphraseSoundModel(int, @NonNull java.util.Locale);
+    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_VOICE_KEYPHRASES) public android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel getKeyphraseSoundModel(int, @NonNull java.util.Locale);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_VOICE_KEYPHRASES) public void updateKeyphraseSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel);
   }
 
 }
@@ -13051,7 +13114,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(@NonNull String, @NonNull java.util.Locale, @Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.AlwaysOnHotwordDetector.Callback);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.HotwordDetector createHotwordDetector(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull android.service.voice.HotwordDetector.Callback);
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.HotwordDetector createHotwordDetector(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.HotwordDetector.Callback);
-    method @NonNull @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public final android.media.voice.KeyphraseModelManager createKeyphraseModelManager();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_VOICE_KEYPHRASES) public final android.media.voice.KeyphraseModelManager createKeyphraseModelManager();
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.VisualQueryDetector createVisualQueryDetector(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.VisualQueryDetector.Callback);
     method @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public void setShouldReceiveSandboxedTrainingData(boolean);
   }
@@ -13514,6 +13577,7 @@
     method public java.util.List<android.telecom.PhoneAccount> getAllPhoneAccounts();
     method public int getAllPhoneAccountsCount();
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<android.telecom.PhoneAccountHandle> getCallCapablePhoneAccounts(boolean);
+    method @FlaggedApi("com.android.internal.telephony.flags.work_profile_api_split") @NonNull @RequiresPermission(allOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.INTERACT_ACROSS_PROFILES}) public java.util.List<android.telecom.PhoneAccountHandle> getCallCapablePhoneAccountsAcrossProfiles(boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}, conditional=true) public int getCallState();
     method public android.telecom.PhoneAccountHandle getConnectionManager();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getCurrentTtyMode();
@@ -15723,6 +15787,7 @@
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isAvailable(int, int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isCapable(int, int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void isSupported(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>) throws android.telephony.ims.ImsException;
+    method @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") public void registerImsEmergencyRegistrationCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.RegistrationManager.RegistrationCallback) throws android.telephony.ims.ImsException;
     method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void registerImsRegistrationCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.ImsMmTelManager.RegistrationCallback) throws android.telephony.ims.ImsException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setAdvancedCallingSettingEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setCrossSimCallingEnabled(boolean) throws android.telephony.ims.ImsException;
@@ -15733,6 +15798,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setVoWiFiRoamingSettingEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setVoWiFiSettingEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setVtSettingEnabled(boolean);
+    method @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") public void unregisterImsEmergencyRegistrationCallback(@NonNull android.telephony.ims.RegistrationManager.RegistrationCallback);
     method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void unregisterImsRegistrationCallback(@NonNull android.telephony.ims.ImsMmTelManager.RegistrationCallback);
   }
 
@@ -15765,6 +15831,8 @@
     ctor public ImsRegistrationAttributes.Builder(int);
     method @NonNull public android.telephony.ims.ImsRegistrationAttributes build();
     method @NonNull public android.telephony.ims.ImsRegistrationAttributes.Builder setFeatureTags(@NonNull java.util.Set<java.lang.String>);
+    method @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") @NonNull public android.telephony.ims.ImsRegistrationAttributes.Builder setFlagRegistrationTypeEmergency();
+    method @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") @NonNull public android.telephony.ims.ImsRegistrationAttributes.Builder setFlagVirtualRegistrationForEmergencyCall();
     method @NonNull public android.telephony.ims.ImsRegistrationAttributes.Builder setSipDetails(@NonNull android.telephony.ims.SipDetails);
   }
 
@@ -16691,6 +16759,7 @@
     ctor public ImsRegistrationImplBase(@NonNull java.util.concurrent.Executor);
     method public final void onDeregistered(android.telephony.ims.ImsReasonInfo);
     method public final void onDeregistered(@Nullable android.telephony.ims.ImsReasonInfo, int, int);
+    method @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") public final void onDeregistered(@Nullable android.telephony.ims.ImsReasonInfo, int, @NonNull android.telephony.ims.ImsRegistrationAttributes);
     method public final void onDeregistered(@Nullable android.telephony.ims.ImsReasonInfo, @NonNull android.telephony.ims.SipDetails);
     method public final void onDeregistered(@Nullable android.telephony.ims.ImsReasonInfo, int, int, @NonNull android.telephony.ims.SipDetails);
     method public final void onRegistered(int);
@@ -16699,6 +16768,7 @@
     method public final void onRegistering(@NonNull android.telephony.ims.ImsRegistrationAttributes);
     method public final void onSubscriberAssociatedUriChanged(android.net.Uri[]);
     method public final void onTechnologyChangeFailed(int, android.telephony.ims.ImsReasonInfo);
+    method @FlaggedApi("com.android.internal.telephony.flags.emergency_registration_state") public final void onTechnologyChangeFailed(@Nullable android.telephony.ims.ImsReasonInfo, @NonNull android.telephony.ims.ImsRegistrationAttributes);
     method public void triggerFullNetworkRegistration(@IntRange(from=100, to=699) int, @Nullable String);
     method public void triggerSipDelegateDeregistration();
     method public void updateSipDelegateRegistration();
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index cef11bb..b2a28b2 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -1995,8 +1995,12 @@
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.ALWAYS_UPDATE_WALLPAPER
 UnflaggedApi: android.Manifest.permission#CAMERA_HEADLESS_SYSTEM_USER:
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.CAMERA_HEADLESS_SYSTEM_USER
+UnflaggedApi: android.Manifest.permission#KEYPHRASE_ENROLLMENT_APPLICATION:
+    New API must be flagged with @FlaggedApi: field android.Manifest.permission.KEYPHRASE_ENROLLMENT_APPLICATION
 UnflaggedApi: android.Manifest.permission#LAUNCH_PERMISSION_SETTINGS:
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.LAUNCH_PERMISSION_SETTINGS
+UnflaggedApi: android.Manifest.permission#MANAGE_VOICE_KEYPHRASES:
+    New API must be flagged with @FlaggedApi: field android.Manifest.permission.MANAGE_VOICE_KEYPHRASES
 UnflaggedApi: android.Manifest.permission#READ_INSTALLED_SESSION_PATHS:
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.READ_INSTALLED_SESSION_PATHS
 UnflaggedApi: android.Manifest.permission#REGISTER_NSD_OFFLOAD_ENGINE:
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a3cd3dc..812ba6d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -369,11 +369,14 @@
   }
 
   public class NotificationManager {
+    method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
     method public void cleanUpCallersAfter(long);
     method public android.content.ComponentName getEffectsSuppressor();
     method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
+    method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
+    method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
     method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel);
   }
 
@@ -507,6 +510,7 @@
     method public int getActivityType();
     method @Nullable public android.graphics.Rect getAppBounds();
     method @NonNull public android.graphics.Rect getBounds();
+    method public int getDisplayRotation();
     method @NonNull public android.graphics.Rect getMaxBounds();
     method public int getRotation();
     method public int getWindowingMode();
@@ -882,7 +886,6 @@
 
   public final class CompanionDeviceManager {
     method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void enableSecureTransport(boolean);
-    field @FlaggedApi("android.companion.companion_transport_apis") public static final int MESSAGE_REQUEST_PING = 1669362552; // 0x63807378
   }
 
   public abstract class CompanionDeviceService extends android.app.Service {
@@ -2133,7 +2136,7 @@
 package android.media.voice {
 
   public final class KeyphraseModelManager {
-    method @RequiresPermission("android.permission.MANAGE_VOICE_KEYPHRASES") public void setModelDatabaseForTestEnabled(boolean);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_VOICE_KEYPHRASES) public void setModelDatabaseForTestEnabled(boolean);
   }
 
 }
@@ -3609,6 +3612,10 @@
     method @Nullable public android.view.View getStatusBarBackgroundView();
   }
 
+  public static final class WindowInsets.Type {
+    method @NonNull public static String toString(int);
+  }
+
   public interface WindowManager extends android.view.ViewManager {
     method public default int getDisplayImePolicy(int);
     method public static boolean hasWindowExtensionsEnabled();
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index ea9bb39..37692d3 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -537,8 +537,8 @@
 
     /**
      * Returns whether the given user requires credential entry at this time. This is used to
-     * intercept activity launches for locked work apps due to work challenge being triggered or
-     * when the profile user is yet to be unlocked.
+     * intercept activity launches for apps corresponding to locked profiles due to separate
+     * challenge being triggered or when the profile user is yet to be unlocked.
      */
     public abstract boolean shouldConfirmCredentials(@UserIdInt int userId);
 
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6ddb36a..6df0f6b 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -407,7 +407,7 @@
 
     private int mLastSessionId;
     // Holds the value of the last reported device ID value from the server for the top activity.
-    int mLastReportedDeviceId;
+    int mLastReportedDeviceId = Context.DEVICE_ID_DEFAULT;
     final ArrayMap<IBinder, CreateServiceData> mServicesData = new ArrayMap<>();
     @UnsupportedAppUsage
     final ArrayMap<IBinder, Service> mServices = new ArrayMap<>();
@@ -4856,10 +4856,13 @@
             service.attach(context, this, data.info.name, data.token, app,
                     ActivityManager.getService());
             if (!service.isUiContext()) { // WindowProviderService is a UI Context.
-                VirtualDeviceManager vdm = context.getSystemService(VirtualDeviceManager.class);
-                if (mLastReportedDeviceId == Context.DEVICE_ID_DEFAULT
-                        || vdm.isValidVirtualDeviceId(mLastReportedDeviceId)) {
+                if (mLastReportedDeviceId == Context.DEVICE_ID_DEFAULT) {
                     service.updateDeviceId(mLastReportedDeviceId);
+                } else {
+                    VirtualDeviceManager vdm = context.getSystemService(VirtualDeviceManager.class);
+                    if (vdm != null && vdm.isValidVirtualDeviceId(mLastReportedDeviceId)) {
+                        service.updateDeviceId(mLastReportedDeviceId);
+                    }
                 }
             }
             service.onCreate();
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 87c86df..287d2bd 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -2909,7 +2909,7 @@
         try {
             return mPM.setPackagesSuspendedAsUser(packageNames, suspended, appExtras,
                     launcherExtras, dialogInfo, flags, mContext.getOpPackageName(),
-                    getUserId());
+                    UserHandle.myUserId() /* suspendingUserId */, getUserId() /* targetUserId */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 9438571..c3adbc3 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -167,7 +167,7 @@
     void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter);
     int getInterruptionFilterFromListener(in INotificationListener token);
     void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
-    void setInterruptionFilter(String pkg, int interruptionFilter);
+    void setInterruptionFilter(String pkg, int interruptionFilter, boolean fromUser);
 
     void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group);
     void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel);
@@ -205,11 +205,11 @@
     @UnsupportedAppUsage
     ZenModeConfig getZenModeConfig();
     NotificationManager.Policy getConsolidatedNotificationPolicy();
-    oneway void setZenMode(int mode, in Uri conditionId, String reason);
+    oneway void setZenMode(int mode, in Uri conditionId, String reason, boolean fromUser);
     oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions);
     boolean isNotificationPolicyAccessGranted(String pkg);
     NotificationManager.Policy getNotificationPolicy(String pkg);
-    void setNotificationPolicy(String pkg, in NotificationManager.Policy policy);
+    void setNotificationPolicy(String pkg, in NotificationManager.Policy policy, boolean fromUser);
     boolean isNotificationPolicyAccessGrantedForPackage(String pkg);
     void setNotificationPolicyAccessGranted(String pkg, boolean granted);
     void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted);
@@ -217,10 +217,10 @@
     Map<String, AutomaticZenRule> getAutomaticZenRules();
     // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
     List<ZenModeConfig.ZenRule> getZenRules();
-    String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg);
-    boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule);
-    boolean removeAutomaticZenRule(String id);
-    boolean removeAutomaticZenRules(String packageName);
+    String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg, boolean fromUser);
+    boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule, boolean fromUser);
+    boolean removeAutomaticZenRule(String id, boolean fromUser);
+    boolean removeAutomaticZenRules(String packageName, boolean fromUser);
     int getRuleInstanceCount(in ComponentName owner);
     void setAutomaticZenRuleState(String id, in Condition condition);
 
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 013bcdd..476232c 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2205,6 +2205,9 @@
 
         private void visitUris(@NonNull Consumer<Uri> visitor) {
             visitIconUri(visitor, getIcon());
+            if (actionIntent != null) {
+                actionIntent.visitUris(visitor);
+            }
         }
 
         @Override
@@ -2898,6 +2901,21 @@
             }
         }
 
+        // allPendingIntents should contain all associated intents after parcelling, but it may also
+        // contain intents added by the app to extras for their own purposes. We only care about
+        // checking the intents known and used by system_server, to avoid the confused deputy issue.
+        List<PendingIntent> pendingIntents = Arrays.asList(contentIntent, deleteIntent,
+                fullScreenIntent);
+        for (PendingIntent intent : pendingIntents) {
+            if (intent != null) {
+                intent.visitUris(visitor);
+            }
+        }
+
+        if (mBubbleMetadata != null) {
+            mBubbleMetadata.visitUris(visitor);
+        }
+
         if (extras != null) {
             visitIconUri(visitor, extras.getParcelable(EXTRA_LARGE_ICON_BIG, Icon.class));
             visitIconUri(visitor, extras.getParcelable(EXTRA_PICTURE_ICON, Icon.class));
@@ -2969,15 +2987,28 @@
                 callPerson.visitUris(visitor);
             }
             visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon.class));
-        }
 
-        if (mBubbleMetadata != null) {
-            visitIconUri(visitor, mBubbleMetadata.getIcon());
-        }
+            // Extras for MediaStyle.
+            PendingIntent deviceIntent = extras.getParcelable(EXTRA_MEDIA_REMOTE_INTENT,
+                    PendingIntent.class);
+            if (deviceIntent != null) {
+                deviceIntent.visitUris(visitor);
+            }
 
-        if (extras != null && extras.containsKey(WearableExtender.EXTRA_WEARABLE_EXTENSIONS)) {
-            WearableExtender extender = new WearableExtender(this);
-            extender.visitUris(visitor);
+            if (extras.containsKey(WearableExtender.EXTRA_WEARABLE_EXTENSIONS)) {
+                WearableExtender extender = new WearableExtender(this);
+                extender.visitUris(visitor);
+            }
+
+            if (extras.containsKey(TvExtender.EXTRA_TV_EXTENDER)) {
+                TvExtender extender = new TvExtender(this);
+                extender.visitUris(visitor);
+            }
+
+            if (extras.containsKey(CarExtender.EXTRA_CAR_EXTENDER)) {
+                CarExtender extender = new CarExtender(this);
+                extender.visitUris(visitor);
+            }
         }
     }
 
@@ -7702,11 +7733,12 @@
             } else if (mPictureIcon.getType() == Icon.TYPE_BITMAP) {
                 // If the icon contains a bitmap, use the old extra so that listeners which look
                 // for that extra can still find the picture. Don't include the new extra in
-                // that case, to avoid duplicating data.
+                // that case, to avoid duplicating data. Leave the unused extra set to null to avoid
+                // crashing apps that came to expect it to be present but null.
                 extras.putParcelable(EXTRA_PICTURE, mPictureIcon.getBitmap());
-                extras.remove(EXTRA_PICTURE_ICON);
+                extras.putParcelable(EXTRA_PICTURE_ICON, null);
             } else {
-                extras.remove(EXTRA_PICTURE);
+                extras.putParcelable(EXTRA_PICTURE, null);
                 extras.putParcelable(EXTRA_PICTURE_ICON, mPictureIcon);
             }
         }
@@ -10558,6 +10590,16 @@
             }
         }
 
+        private void visitUris(@NonNull Consumer<Uri> visitor) {
+            visitIconUri(visitor, getIcon());
+            if (mPendingIntent != null) {
+                mPendingIntent.visitUris(visitor);
+            }
+            if (mDeleteIntent != null) {
+                mDeleteIntent.visitUris(visitor);
+            }
+        }
+
         /**
          * Builder to construct a {@link BubbleMetadata} object.
          */
@@ -11756,6 +11798,9 @@
         }
 
         private void visitUris(@NonNull Consumer<Uri> visitor) {
+            if (mDisplayIntent != null) {
+                mDisplayIntent.visitUris(visitor);
+            }
             for (Action action : mActions) {
                 action.visitUris(visitor);
             }
@@ -11908,12 +11953,19 @@
 
         /**
          * Returns the unread conversation conveyed by this notification.
+         *
          * @see #setUnreadConversation(UnreadConversation)
          */
         public UnreadConversation getUnreadConversation() {
             return mUnreadConversation;
         }
 
+        private void visitUris(@NonNull Consumer<Uri> visitor) {
+            if (mUnreadConversation != null) {
+                mUnreadConversation.visitUris(visitor);
+            }
+        }
+
         /**
          * A class which holds the unread messages from a conversation.
          */
@@ -12065,7 +12117,16 @@
                         onRead,
                         participants, b.getLong(KEY_TIMESTAMP));
             }
-        };
+
+            private void visitUris(@NonNull Consumer<Uri> visitor) {
+                if (mReadPendingIntent != null) {
+                    mReadPendingIntent.visitUris(visitor);
+                }
+                if (mReplyPendingIntent != null) {
+                    mReplyPendingIntent.visitUris(visitor);
+                }
+            }
+        }
 
         /**
          * Builder class for {@link CarExtender.UnreadConversation} objects.
@@ -12388,6 +12449,15 @@
         public boolean isSuppressShowOverApps() {
             return mSuppressShowOverApps;
         }
+
+        private void visitUris(@NonNull Consumer<Uri> visitor) {
+            if (mContentIntent != null) {
+                mContentIntent.visitUris(visitor);
+            }
+            if (mDeleteIntent != null) {
+                mDeleteIntent.visitUris(visitor);
+            }
+        }
     }
 
     /**
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index d23b16d..0b6e24c 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1184,14 +1184,20 @@
      */
     @UnsupportedAppUsage
     public void setZenMode(int mode, Uri conditionId, String reason) {
+        setZenMode(mode, conditionId, reason, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            service.setZenMode(mode, conditionId, reason);
+            service.setZenMode(mode, conditionId, reason, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
+
     /**
      * @hide
      */
@@ -1325,9 +1331,19 @@
      * @return The id of the newly created rule; null if the rule could not be created.
      */
     public String addAutomaticZenRule(AutomaticZenRule automaticZenRule) {
+        return addAutomaticZenRule(automaticZenRule, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @NonNull
+    public String addAutomaticZenRule(@NonNull AutomaticZenRule automaticZenRule,
+            boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.addAutomaticZenRule(automaticZenRule, mContext.getPackageName());
+            return service.addAutomaticZenRule(automaticZenRule,
+                    mContext.getPackageName(), fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1347,9 +1363,17 @@
      * @return Whether the rule was successfully updated.
      */
     public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
+        return updateAutomaticZenRule(id, automaticZenRule, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean updateAutomaticZenRule(@NonNull String id,
+            @NonNull AutomaticZenRule automaticZenRule, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.updateAutomaticZenRule(id, automaticZenRule);
+            return service.updateAutomaticZenRule(id, automaticZenRule, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1388,9 +1412,16 @@
      * @return Whether the rule was successfully deleted.
      */
     public boolean removeAutomaticZenRule(String id) {
+        return removeAutomaticZenRule(id, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public boolean removeAutomaticZenRule(@NonNull String id, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.removeAutomaticZenRule(id);
+            return service.removeAutomaticZenRule(id, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1402,9 +1433,14 @@
      * @hide
      */
     public boolean removeAutomaticZenRules(String packageName) {
+        return removeAutomaticZenRules(packageName, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public boolean removeAutomaticZenRules(String packageName, boolean fromUser) {
         INotificationManager service = getService();
         try {
-            return service.removeAutomaticZenRules(packageName);
+            return service.removeAutomaticZenRules(packageName, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1685,10 +1721,15 @@
      */
     // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
+        setNotificationPolicy(policy, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public void setNotificationPolicy(@NonNull Policy policy, boolean fromUser) {
         checkRequired("policy", policy);
         INotificationManager service = getService();
         try {
-            service.setNotificationPolicy(mContext.getOpPackageName(), policy);
+            service.setNotificationPolicy(mContext.getOpPackageName(), policy, fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2685,9 +2726,16 @@
      */
     // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
+        setInterruptionFilter(interruptionFilter, /* fromUser= */ false);
+    }
+
+    /** @hide */
+    public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter,
+            boolean fromUser) {
         final INotificationManager service = getService();
         try {
-            service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter);
+            service.setInterruptionFilter(mContext.getOpPackageName(), interruptionFilter,
+                    fromUser);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 772b0b4..47d19ed 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -88,6 +88,10 @@
 # Pinner
 per-file pinner-client.aconfig = file:/core/java/android/app/pinner/OWNERS
 
+# BackgroundInstallControlManager
+per-file BackgroundInstallControlManager.java = file:/services/core/java/com/android/server/pm/OWNERS
+per-file background_install_control_manager.aconfig = file:/services/core/java/com/android/server/pm/OWNERS
+
 # ResourcesManager
 per-file ResourcesManager.java = file:RESOURCES_OWNERS
 
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 62209b0..0261f0a 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -44,6 +44,8 @@
 import android.content.pm.PackageManager.ResolveInfoFlagsBits;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -69,6 +71,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * A description of an Intent and target action to perform with it.  Instances
@@ -1460,6 +1463,21 @@
         return sb.toString();
     }
 
+    /**
+     * See {@link Intent#visitUris(Consumer)}.
+     *
+     * @hide
+     */
+    public void visitUris(@NonNull Consumer<Uri> visitor) {
+        if (android.app.Flags.visitRiskyUris()) {
+            Intent intent = Binder.withCleanCallingIdentity(this::getIntent);
+
+            if (intent != null) {
+                intent.visitUris(visitor);
+            }
+        }
+    }
+
     /** @hide */
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 820ff3e..be420de 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -77,6 +77,7 @@
 import android.os.RemoteException;
 import android.os.StrictMode;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -91,7 +92,6 @@
 import libcore.io.IoUtils;
 
 import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -614,11 +614,14 @@
                 ColorManagementProxy cmProxy) {
             if (mService != null) {
                 try {
+                    Trace.beginSection("WPMS.isWallpaperSupported");
                     if (!mService.isWallpaperSupported(context.getOpPackageName())) {
                         return null;
                     }
                 } catch (RemoteException e) {
                     throw e.rethrowFromSystemServer();
+                } finally {
+                    Trace.endSection();
                 }
             }
             synchronized (this) {
@@ -629,6 +632,7 @@
                 mCachedWallpaper = null;
                 Bitmap currentWallpaper = null;
                 try {
+                    Trace.beginSection("WPMS.getCurrentWallpaperLocked");
                     currentWallpaper = getCurrentWallpaperLocked(
                             context, which, userId, hardware, cmProxy);
                 } catch (OutOfMemoryError e) {
@@ -654,6 +658,8 @@
                         // Post-O apps really most sincerely need the permission.
                         throw e;
                     }
+                } finally {
+                    Trace.endSection();
                 }
                 if (currentWallpaper != null) {
                     mCachedWallpaper = new CachedWallpaper(currentWallpaper, userId, which);
@@ -732,19 +738,15 @@
 
             try {
                 Bundle params = new Bundle();
+                Trace.beginSection("WPMS.getWallpaperWithFeature_" + which);
                 ParcelFileDescriptor pfd = mService.getWallpaperWithFeature(
                         context.getOpPackageName(), context.getAttributionTag(), this, which,
                         params, userId, /* getCropped = */ true);
+                Trace.endSection();
 
                 if (pfd != null) {
-                    try (BufferedInputStream bis = new BufferedInputStream(
-                            new ParcelFileDescriptor.AutoCloseInputStream(pfd))) {
-                        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                        int data;
-                        while ((data = bis.read()) != -1) {
-                            baos.write(data);
-                        }
-                        ImageDecoder.Source src = ImageDecoder.createSource(baos.toByteArray());
+                    try (InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+                        ImageDecoder.Source src = ImageDecoder.createSource(is.readAllBytes());
                         return ImageDecoder.decodeBitmap(src, ((decoder, info, source) -> {
                             // Mutable and hardware config can't be set at the same time.
                             decoder.setMutableRequired(!hardware);
@@ -764,13 +766,18 @@
         }
 
         private Bitmap getDefaultWallpaper(Context context, @SetWallpaperFlags int which) {
+            Trace.beginSection("WPMS.getDefaultWallpaper_" + which);
             Bitmap defaultWallpaper = mDefaultWallpaper;
             if (defaultWallpaper == null || defaultWallpaper.isRecycled()) {
                 defaultWallpaper = null;
+                Trace.beginSection("WPMS.openDefaultWallpaper");
                 try (InputStream is = openDefaultWallpaper(context, which)) {
+                    Trace.endSection();
                     if (is != null) {
                         BitmapFactory.Options options = new BitmapFactory.Options();
+                        Trace.beginSection("WPMS.decodeStream");
                         defaultWallpaper = BitmapFactory.decodeStream(is, null, options);
+                        Trace.endSection();
                     }
                 } catch (OutOfMemoryError | IOException e) {
                     Log.w(TAG, "Can't decode stream", e);
@@ -779,6 +786,7 @@
             synchronized (this) {
                 mDefaultWallpaper = defaultWallpaper;
             }
+            Trace.endSection();
             return defaultWallpaper;
         }
 
diff --git a/core/java/android/app/WindowConfiguration.java b/core/java/android/app/WindowConfiguration.java
index 4621634..aa3b71a 100644
--- a/core/java/android/app/WindowConfiguration.java
+++ b/core/java/android/app/WindowConfiguration.java
@@ -27,6 +27,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.res.Configuration;
@@ -326,7 +327,7 @@
     }
 
     /**
-     * Sets the apparent display cutout.
+     * Sets the display rotation.
      * @hide
      */
     public void setDisplayRotation(@Surface.Rotation int rotation) {
@@ -386,9 +387,9 @@
     }
 
     /**
-     * @see #setDisplayRotation
-     * @hide
+     * Gets the display rotation.
      */
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
     public @Surface.Rotation int getDisplayRotation() {
         return mDisplayRotation;
     }
diff --git a/core/java/android/app/usage/StorageStats.java b/core/java/android/app/usage/StorageStats.java
index 8d25d7b..87d97d5 100644
--- a/core/java/android/app/usage/StorageStats.java
+++ b/core/java/android/app/usage/StorageStats.java
@@ -17,11 +17,16 @@
 package android.app.usage;
 
 import android.annotation.BytesLong;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.content.Context;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.UserHandle;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Storage statistics for a UID, package, or {@link UserHandle} on a single
  * storage volume.
@@ -29,10 +34,47 @@
  * @see StorageStatsManager
  */
 public final class StorageStats implements Parcelable {
-    /** {@hide} */ public long codeBytes;
-    /** {@hide} */ public long dataBytes;
-    /** {@hide} */ public long cacheBytes;
-    /** {@hide} */ public long externalCacheBytes;
+    /** @hide */ public long codeBytes;
+    /** @hide */ public long dataBytes;
+    /** @hide */ public long cacheBytes;
+    /** @hide */ public long apkBytes;
+    /** @hide */ public long libBytes;
+    /** @hide */ public long dmBytes;
+    /** @hide */ public long externalCacheBytes;
+
+    /** Represents all .apk files in application code path.
+     * Can be used as an input to {@link #getAppBytesByDataType(int)}
+     * to get the sum of sizes for files of this type.
+     */
+    @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+    public static final int APP_DATA_TYPE_FILE_TYPE_APK = 0;
+
+    /** Represents all .dm files in application code path.
+     * Can be used as an input to {@link #getAppBytesByDataType(int)}
+     * to get the sum of sizes for files of this type.
+     */
+    @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+    public static final int APP_DATA_TYPE_FILE_TYPE_DM = 1;
+
+    /** Represents lib/ in application code path.
+     * Can be used as an input to {@link #getAppBytesByDataType(int)}
+     * to get the size of lib/ directory.
+     */
+    @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+    public static final int APP_DATA_TYPE_LIB = 2;
+
+    /**
+     * Keep in sync with the file types defined above.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+    @IntDef(flag = false, value = {
+        APP_DATA_TYPE_FILE_TYPE_APK,
+        APP_DATA_TYPE_FILE_TYPE_DM,
+        APP_DATA_TYPE_LIB,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AppDataType {}
 
     /**
      * Return the size of app. This includes {@code APK} files, optimized
@@ -48,6 +90,27 @@
     }
 
     /**
+     * Return the size of the specified data type. This includes files stored under
+     * application code path.
+     * <p>
+     * If there is more than one package inside a uid, the return represents the aggregated
+     * stats when query StorageStat for package or uid.
+     * The data  is not collected and the return defaults to 0 when query StorageStats for user.
+     *
+     * <p>
+     * Data is isolated for each user on a multiuser device.
+     */
+    @FlaggedApi(Flags.FLAG_GET_APP_BYTES_BY_DATA_TYPE_API)
+    public long getAppBytesByDataType(@AppDataType int dataType) {
+        switch (dataType) {
+          case APP_DATA_TYPE_FILE_TYPE_APK: return apkBytes;
+          case APP_DATA_TYPE_LIB: return libBytes;
+          case APP_DATA_TYPE_FILE_TYPE_DM: return dmBytes;
+          default: return 0;
+        }
+    }
+
+    /**
      * Return the size of all data. This includes files stored under
      * {@link Context#getDataDir()}, {@link Context#getCacheDir()},
      * {@link Context#getCodeCacheDir()}.
@@ -98,6 +161,9 @@
         this.codeBytes = in.readLong();
         this.dataBytes = in.readLong();
         this.cacheBytes = in.readLong();
+        this.apkBytes = in.readLong();
+        this.libBytes = in.readLong();
+        this.dmBytes = in.readLong();
         this.externalCacheBytes = in.readLong();
     }
 
@@ -111,6 +177,9 @@
         dest.writeLong(codeBytes);
         dest.writeLong(dataBytes);
         dest.writeLong(cacheBytes);
+        dest.writeLong(apkBytes);
+        dest.writeLong(libBytes);
+        dest.writeLong(dmBytes);
         dest.writeLong(externalCacheBytes);
     }
 
diff --git a/core/java/android/app/usage/UsageEventsQuery.java b/core/java/android/app/usage/UsageEventsQuery.java
index 8c63d18..3cd2923 100644
--- a/core/java/android/app/usage/UsageEventsQuery.java
+++ b/core/java/android/app/usage/UsageEventsQuery.java
@@ -19,9 +19,11 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.app.usage.UsageEvents.Event;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.UserHandle;
 import android.util.ArraySet;
 
 import com.android.internal.util.ArrayUtils;
@@ -40,11 +42,13 @@
     private final @CurrentTimeMillisLong long mBeginTimeMillis;
     private final @CurrentTimeMillisLong long mEndTimeMillis;
     private final @Event.EventType int[] mEventTypes;
+    private final @UserIdInt int mUserId;
 
     private UsageEventsQuery(@NonNull Builder builder) {
         mBeginTimeMillis = builder.mBeginTimeMillis;
         mEndTimeMillis = builder.mEndTimeMillis;
         mEventTypes = ArrayUtils.convertToIntArray(builder.mEventTypes);
+        mUserId = builder.mUserId;
     }
 
     private UsageEventsQuery(Parcel in) {
@@ -53,6 +57,7 @@
         int eventTypesLength = in.readInt();
         mEventTypes = new int[eventTypesLength];
         in.readIntArray(mEventTypes);
+        mUserId = in.readInt();
     }
 
     /**
@@ -87,6 +92,11 @@
         return eventTypeSet;
     }
 
+    /** @hide */
+    public @UserIdInt int getUserId() {
+        return mUserId;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -98,6 +108,7 @@
         dest.writeLong(mEndTimeMillis);
         dest.writeInt(mEventTypes.length);
         dest.writeIntArray(mEventTypes);
+        dest.writeInt(mUserId);
     }
 
     @NonNull
@@ -126,6 +137,7 @@
         private final @CurrentTimeMillisLong long mBeginTimeMillis;
         private final @CurrentTimeMillisLong long mEndTimeMillis;
         private final ArraySet<Integer> mEventTypes = new ArraySet<>();
+        private @UserIdInt int mUserId = UserHandle.USER_NULL;
 
         /**
          * Constructor that specifies the period for which to return events.
@@ -169,5 +181,15 @@
             }
             return this;
         }
+
+        /**
+         * Specifices the user id for the query.
+         * @param userId for whom the query should be performed.
+         * @hide
+         */
+        public @NonNull Builder setUserId(@UserIdInt int userId) {
+            mUserId = userId;
+            return this;
+        }
     }
 }
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index a611255..4d9d911 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -35,3 +35,10 @@
     description: " Feature flag to support filter based event query API"
     bug: "194321117"
 }
+
+flag {
+    name: "get_app_bytes_by_data_type_api"
+    namespace: "system_performance"
+    description: "Feature flag for collecting app data size by file type API"
+    bug: "294088945"
+}
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index c95b864..ec2e5fe 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -12,4 +12,11 @@
   namespace: "app_widgets"
   description: "Enable adapter conversion to RemoteCollectionItemsAdapter"
   bug: "245950570"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "remove_app_widget_service_io_from_critical_path"
+  namespace: "app_widgets"
+  description: "Move state file IO to non-critical path"
+  bug: "312949280"
+}
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index d5b5f40..879656a 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -20,7 +20,6 @@
 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION;
 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER;
 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH;
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
@@ -219,32 +218,43 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @TestApi public static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
+    public static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
+    /**
+     * Test message type without a response.
+     *
+     * @hide
+     */
+    public static final int MESSAGE_ONEWAY_PING = 0x43807378; // +PIN
     /**
      * Message header assigned to the remote authentication handshakes.
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     public static final int MESSAGE_REQUEST_REMOTE_AUTHENTICATION = 0x63827765; // ?RMA
     /**
      * Message header assigned to the telecom context sync metadata.
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     public static final int MESSAGE_REQUEST_CONTEXT_SYNC = 0x63678883; // ?CXS
     /**
      * Message header assigned to the permission restore request.
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     public static final int MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983; // ?RES
+    /**
+     * Message header assigned to the one-way message sent from the wearable device.
+     *
+     * @hide
+     */
+    public static final int MESSAGE_ONEWAY_FROM_WEARABLE = 0x43708287; // +FRW
+    /**
+     * Message header assigned to the one-way message sent to the wearable device.
+     *
+     * @hide
+     */
+    public static final int MESSAGE_ONEWAY_TO_WEARABLE = 0x43847987; // +TOW
 
     /**
      * The length limit of Association tag.
@@ -905,8 +915,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     public interface OnTransportsChangedListener {
         /**
          * Invoked when a transport is attached or detached.
@@ -925,8 +933,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void addOnTransportsChangedListener(
             @NonNull @CallbackExecutor Executor executor,
@@ -947,8 +953,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void removeOnTransportsChangedListener(
             @NonNull OnTransportsChangedListener listener) {
@@ -969,8 +973,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void sendMessage(int messageType, @NonNull byte[] data, @NonNull int[] associationIds) {
         try {
@@ -987,8 +989,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     public interface OnMessageReceivedListener {
         /**
          * Called when a message is received.
@@ -1001,8 +1001,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void addOnMessageReceivedListener(
             @NonNull @CallbackExecutor Executor executor, int messageType,
@@ -1021,8 +1019,6 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_COMPANION_TRANSPORT_APIS)
-    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void removeOnMessageReceivedListener(int messageType,
             @NonNull OnMessageReceivedListener listener) {
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
index fac44b5..44942d6 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -16,11 +16,11 @@
 package android.companion.virtual.camera;
 
 import android.companion.virtual.camera.VirtualCameraStreamConfig;
-import android.companion.virtual.camera.VirtualCameraMetadata;
 import android.view.Surface;
 
 /**
- * Interface for the virtual camera service and system server to talk back to the virtual camera owner.
+ * Interface for the virtual camera service and system server to talk back to the virtual camera
+ * owner.
  *
  * @hide
  */
@@ -40,8 +40,7 @@
             in VirtualCameraStreamConfig streamConfig);
 
     /**
-     * The client application is requesting a camera frame for the given streamId with the provided
-     * metadata.
+     * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
      * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
@@ -52,16 +51,14 @@
      *     VirtualCameraStreamConfig)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
-     * @param metadata The metadata requested for the frame. The virtual camera should do its best
-     *     to honor the requested metadata.
      */
-    oneway void onProcessCaptureRequest(
-            int streamId, long frameId, in VirtualCameraMetadata metadata);
+    oneway void onProcessCaptureRequest(int streamId, long frameId);
 
     /**
      * The stream previously configured when {@link #onStreamConfigured(int, Surface,
      * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
-     * freed. The Surface was disposed on the client side and should not be used anymore by the virtual camera owner
+     * freed. The Surface was disposed on the client side and should not be used anymore by the
+     * virtual camera owner.
      *
      * @param streamId The id of the stream that was closed.
      */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index a18ae03..5b658b8 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -18,7 +18,6 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.companion.virtual.flags.Flags;
 import android.view.Surface;
@@ -50,8 +49,7 @@
             @NonNull VirtualCameraStreamConfig streamConfig);
 
     /**
-     * The client application is requesting a camera frame for the given streamId with the provided
-     * metadata.
+     * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
      * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
@@ -62,12 +60,8 @@
      *     VirtualCameraStreamConfig)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
-     * @param metadata The metadata requested for the frame. The virtual camera should do its best
-     *     to honor the requested metadata but the consumer won't be informed about the metadata set
-     *     for a particular frame. If null, the requested frame can be anything the producer sends.
      */
-    void onProcessCaptureRequest(
-            int streamId, long frameId, @Nullable VirtualCameraMetadata metadata);
+    default void onProcessCaptureRequest(int streamId, long frameId) {}
 
     /**
      * The stream previously configured when {@link #onStreamConfigured(int, Surface,
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index f1eb240..59fe9a1 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -20,11 +20,9 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
-import android.annotation.StringRes;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.companion.virtual.flags.Flags;
-import android.content.res.Resources;
 import android.graphics.ImageFormat;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -45,16 +43,16 @@
 @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
 public final class VirtualCameraConfig implements Parcelable {
 
-    private final @StringRes int mNameStringRes;
+    private final String mName;
     private final Set<VirtualCameraStreamConfig> mStreamConfigurations;
     private final IVirtualCameraCallback mCallback;
 
     private VirtualCameraConfig(
-            int displayNameStringRes,
+            @NonNull String name,
             @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
             @NonNull Executor executor,
             @NonNull VirtualCameraCallback callback) {
-        mNameStringRes = displayNameStringRes;
+        mName = requireNonNull(name, "Missing name");
         mStreamConfigurations =
                 Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations"));
         if (mStreamConfigurations.isEmpty()) {
@@ -68,7 +66,7 @@
     }
 
     private VirtualCameraConfig(@NonNull Parcel in) {
-        mNameStringRes = in.readInt();
+        mName = in.readString8();
         mCallback = IVirtualCameraCallback.Stub.asInterface(in.readStrongBinder());
         mStreamConfigurations =
                 Set.of(
@@ -84,18 +82,18 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeInt(mNameStringRes);
+        dest.writeString8(mName);
         dest.writeStrongInterface(mCallback);
         dest.writeParcelableArray(
                 mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags);
     }
 
     /**
-     * @return The display name of this VirtualCamera
+     * @return The name of this VirtualCamera
      */
-    @StringRes
-    public int getDisplayNameStringRes() {
-        return mNameStringRes;
+    @NonNull
+    public String getName() {
+        return mName;
     }
 
     /**
@@ -126,30 +124,22 @@
      * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}.
      * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
      *     VirtualCameraCallback)}
-     * <li>A user readable name can be set with {@link #setDisplayNameStringRes(int)}
+     * <li>A camera name must be set with {@link #setName(String)}
      */
     @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
     public static final class Builder {
 
-        private @StringRes int mDisplayNameStringRes = Resources.ID_NULL;
+        private String mName;
         private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>();
         private Executor mCallbackExecutor;
         private VirtualCameraCallback mCallback;
 
         /**
-         * Set the visible name of this camera for the user.
-         *
-         * <p>Sets the resource to a string representing a user readable name for this virtual
-         * camera.
-         *
-         * @throws IllegalArgumentException if an invalid resource id is passed.
+         * Set the name of the virtual camera instance.
          */
         @NonNull
-        public Builder setDisplayNameStringRes(@StringRes int displayNameStringRes) {
-            if (displayNameStringRes <= 0) {
-                throw new IllegalArgumentException("Invalid resource passed for display name");
-            }
-            mDisplayNameStringRes = displayNameStringRes;
+        public Builder setName(@NonNull String name) {
+            mName = requireNonNull(name, "Display name cannot be null");
             return this;
         }
 
@@ -203,7 +193,7 @@
         @NonNull
         public VirtualCameraConfig build() {
             return new VirtualCameraConfig(
-                    mDisplayNameStringRes, mStreamConfigurations, mCallbackExecutor, mCallback);
+                    mName, mStreamConfigurations, mCallbackExecutor, mCallback);
         }
     }
 
@@ -224,9 +214,8 @@
         }
 
         @Override
-        public void onProcessCaptureRequest(
-                int streamId, long frameId, VirtualCameraMetadata metadata) {
-            mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId, metadata));
+        public void onProcessCaptureRequest(int streamId, long frameId) {
+            mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId));
         }
 
         @Override
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
deleted file mode 100644
index 1ba36d0..0000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
+++ /dev/null
@@ -1,61 +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 android.companion.virtual.camera;
-
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * Data structure used to store camera metadata compatible with VirtualCamera.
- *
- * @hide
- */
-@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
-public final class VirtualCameraMetadata implements Parcelable {
-
-    /** @hide */
-    public VirtualCameraMetadata(@NonNull Parcel in) {}
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {}
-
-    @NonNull
-    public static final Creator<VirtualCameraMetadata> CREATOR =
-            new Creator<>() {
-                @Override
-                @NonNull
-                public VirtualCameraMetadata createFromParcel(Parcel in) {
-                    return new VirtualCameraMetadata(in);
-                }
-
-                @Override
-                @NonNull
-                public VirtualCameraMetadata[] newArray(int size) {
-                    return new VirtualCameraMetadata[size];
-                }
-            };
-}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index e198821..1272f16 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -24,6 +24,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Objects;
+
 /**
  * The configuration of a single virtual camera stream.
  *
@@ -98,6 +100,19 @@
         return mHeight;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o;
+        return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mWidth, mHeight, mFormat);
+    }
+
     /** Returns the {@link ImageFormat} of this stream. */
     @ImageFormat.Format
     public int getFormat() {
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 7af0be3..183b9b0 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -101,6 +101,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TimeZone;
+import java.util.function.Consumer;
 
 /**
  * An intent is an abstract description of an operation to be performed.  It
@@ -8147,6 +8148,27 @@
         }
     }
 
+    /**
+     * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission
+     * grants will need to be issued to ensure the recipient of this object is able to render its
+     * contents.
+     * See b/281044385 for more context and examples about what happens when this isn't done
+     * correctly.
+     *
+     * @hide
+     */
+    public void visitUris(@NonNull Consumer<Uri> visitor) {
+        if (android.app.Flags.visitRiskyUris()) {
+            visitor.accept(mData);
+            if (mSelector != null) {
+                mSelector.visitUris(visitor);
+            }
+            if (mOriginalIntent != null) {
+                mOriginalIntent.visitUris(visitor);
+            }
+        }
+    }
+
     public static Intent getIntentOld(String uri) throws URISyntaxException {
         Intent intent = getIntentOld(uri, 0);
         intent.mLocalFlags |= LOCAL_FLAG_FROM_URI;
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index d13d962..12da665 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1393,6 +1393,18 @@
     public static final long OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION = 255940284L;
 
     /**
+     * Enables {@link #SCREEN_ORIENTATION_USER} which overrides any orientation requested
+     * by the activity. Fixed orientation apps can be overridden to fullscreen on large
+     * screen devices with ignoreOrientationRequest enabled with this override.
+     *
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_ANY_ORIENTATION_TO_USER = 310816437L;
+
+    /**
      * Compares activity window layout min width/height with require space for multi window to
      * determine if it can be put into multi window mode.
      */
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 98623de..6dc8d47 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -300,7 +300,8 @@
 
     String[] setPackagesSuspendedAsUser(in String[] packageNames, boolean suspended,
             in PersistableBundle appExtras, in PersistableBundle launcherExtras,
-            in SuspendDialogInfo dialogInfo, int flags, String callingPackage, int userId);
+            in SuspendDialogInfo dialogInfo, int flags, String suspendingPackage,
+            int suspendingUserId, int targetUserId);
 
     String[] getUnsuspendablePackagesForUser(in String[] packageNames, int userId);
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 82a8c11..a863870 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -10000,6 +10000,9 @@
      * device administrators or apps holding {@link android.Manifest.permission#MANAGE_USERS} or
      * {@link android.Manifest.permission#SUSPEND_APPS}.
      *
+     * <p>
+     * <strong>Note:</strong>This API doesn't support cross user suspension and should only be used
+     * for testing.
      * @param suspendedPackage The package that has been suspended.
      * @return Name of the package that suspended the given package. Returns {@code null} if the
      * given package is not currently suspended and the platform package name - i.e.
diff --git a/core/java/android/content/pm/PackageStats.java b/core/java/android/content/pm/PackageStats.java
index 81cfc07..b919c4b 100644
--- a/core/java/android/content/pm/PackageStats.java
+++ b/core/java/android/content/pm/PackageStats.java
@@ -55,6 +55,18 @@
     /** Size of cache used by the application. (e.g., /data/data/<app>/cache) */
     public long cacheSize;
 
+    /** Size of .apk files of the application. */
+    /** @hide */
+    public long apkSize;
+
+    /** Size of the libraries of the application. */
+    /** @hide */
+    public long libSize;
+
+    /** Size of the .dm files of the application. */
+    /** @hide */
+    public long dmSize;
+
     /**
      * Size of the secure container on external storage holding the
      * application's code.
@@ -108,6 +120,18 @@
             sb.append(" cache=");
             sb.append(cacheSize);
         }
+        if (apkSize != 0) {
+            sb.append(" apk=");
+            sb.append(apkSize);
+        }
+        if (libSize != 0) {
+            sb.append(" lib=");
+            sb.append(libSize);
+        }
+        if (dmSize != 0) {
+            sb.append(" dm=");
+            sb.append(dmSize);
+        }
         if (externalCodeSize != 0) {
             sb.append(" extCode=");
             sb.append(externalCodeSize);
@@ -149,6 +173,9 @@
         codeSize = source.readLong();
         dataSize = source.readLong();
         cacheSize = source.readLong();
+        apkSize = source.readLong();
+        libSize = source.readLong();
+        dmSize = source.readLong();
         externalCodeSize = source.readLong();
         externalDataSize = source.readLong();
         externalCacheSize = source.readLong();
@@ -162,6 +189,9 @@
         codeSize = pStats.codeSize;
         dataSize = pStats.dataSize;
         cacheSize = pStats.cacheSize;
+        apkSize = pStats.apkSize;
+        libSize = pStats.libSize;
+        dmSize = pStats.dmSize;
         externalCodeSize = pStats.externalCodeSize;
         externalDataSize = pStats.externalDataSize;
         externalCacheSize = pStats.externalCacheSize;
@@ -179,6 +209,9 @@
         dest.writeLong(codeSize);
         dest.writeLong(dataSize);
         dest.writeLong(cacheSize);
+        dest.writeLong(apkSize);
+        dest.writeLong(libSize);
+        dest.writeLong(dmSize);
         dest.writeLong(externalCodeSize);
         dest.writeLong(externalDataSize);
         dest.writeLong(externalCacheSize);
@@ -198,6 +231,9 @@
                 && codeSize == otherStats.codeSize
                 && dataSize == otherStats.dataSize
                 && cacheSize == otherStats.cacheSize
+                && apkSize == otherStats.apkSize
+                && libSize == otherStats.libSize
+                && dmSize == otherStats.dmSize
                 && externalCodeSize == otherStats.externalCodeSize
                 && externalDataSize == otherStats.externalDataSize
                 && externalCacheSize == otherStats.externalCacheSize
@@ -208,7 +244,8 @@
     @Override
     public int hashCode() {
         return Objects.hash(packageName, userHandle, codeSize, dataSize,
-                cacheSize, externalCodeSize, externalDataSize, externalCacheSize, externalMediaSize,
+                apkSize, libSize, dmSize, cacheSize, externalCodeSize,
+                externalDataSize, externalCacheSize, externalMediaSize,
                 externalObbSize);
     }
 
diff --git a/core/java/android/content/pm/SigningDetails.java b/core/java/android/content/pm/SigningDetails.java
index 8c21974..bb09ad2 100644
--- a/core/java/android/content/pm/SigningDetails.java
+++ b/core/java/android/content/pm/SigningDetails.java
@@ -31,6 +31,8 @@
 
 import libcore.util.HexEncoding;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.security.PublicKey;
 import java.security.cert.CertificateException;
 import java.util.ArrayList;
@@ -49,6 +51,7 @@
 
     private static final String TAG = "SigningDetails";
 
+    @Retention(RetentionPolicy.SOURCE)
     @IntDef({SignatureSchemeVersion.UNKNOWN,
             SignatureSchemeVersion.JAR,
             SignatureSchemeVersion.SIGNING_BLOCK_V2,
diff --git a/core/java/android/content/pm/SigningInfo.java b/core/java/android/content/pm/SigningInfo.java
index a407704..23daaf2 100644
--- a/core/java/android/content/pm/SigningInfo.java
+++ b/core/java/android/content/pm/SigningInfo.java
@@ -17,9 +17,9 @@
 package android.content.pm;
 
 import android.annotation.FlaggedApi;
-import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.pm.SigningDetails.SignatureSchemeVersion;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArraySet;
@@ -53,7 +53,7 @@
      * schemas</a>
      */
     @FlaggedApi(Flags.FLAG_ARCHIVING)
-    public SigningInfo(@IntRange(from = 0) int schemeVersion,
+    public SigningInfo(@SignatureSchemeVersion int schemeVersion,
             @Nullable Collection<Signature> apkContentsSigners,
             @Nullable Collection<PublicKey> publicKeys,
             @Nullable Collection<Signature> signingCertificateHistory) {
@@ -168,7 +168,7 @@
      * schemas</a>
      */
     @FlaggedApi(Flags.FLAG_ARCHIVING)
-    public @IntRange(from = 0) int getSchemeVersion() {
+    public @SignatureSchemeVersion int getSchemeVersion() {
         return mSigningDetails.getSignatureSchemeVersion();
     }
 
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index b04b7ba..94bec35 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -115,3 +115,19 @@
     description: "Feature flag to fix duplicated PackageManager flag values"
     bug: "314815969"
 }
+
+flag {
+    name: "provide_info_of_apk_in_apex"
+    namespace: "package_manager_service"
+    description: "Feature flag to provide the information of APK-in-APEX"
+    bug: "306329516"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "improve_home_app_behavior"
+    namespace: "package_manager_service"
+    description: "Feature flag to improve the uninstallation and preferred activity of home app."
+    bug: "310801107"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 57025c2..9a1796f 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -56,3 +56,11 @@
     description: "Add support to lock private space automatically after a time period"
     bug: "303201022"
 }
+
+flag {
+    name: "avatar_sync"
+    namespace: "multiuser"
+    description: "Implement new Avatar Picker outside of SetttingsLib with ability to select avatars from Google Account and synchronise to any changes."
+    bug: "296829976"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/res/FontScaleConverter.java b/core/java/android/content/res/FontScaleConverter.java
index 28525e2..088949e 100644
--- a/core/java/android/content/res/FontScaleConverter.java
+++ b/core/java/android/content/res/FontScaleConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,15 +16,11 @@
 
 package android.content.res;
 
-import android.annotation.NonNull;
-import android.util.MathUtils;
 
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Arrays;
+import android.annotation.FlaggedApi;
 
 /**
- * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
+ * A converter for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
  * "dp" dimension according to a non-linear curve.
  *
  * <p>This is meant to improve readability at larger font scales: larger fonts will scale up more
@@ -32,131 +28,16 @@
  *
  * <p>The thinking here is that large fonts are already big enough to read, but we still want to
  * scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
- *
- * @hide
  */
-public class FontScaleConverter {
-
-    @VisibleForTesting
-    final float[] mFromSpValues;
-    @VisibleForTesting
-    final float[] mToDpValues;
+@FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
+public interface FontScaleConverter {
+    /**
+     * Converts a dimension in "sp" to "dp".
+     */
+    float convertSpToDp(float sp);
 
     /**
-     * Creates a lookup table for the given conversions.
-     *
-     * <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
-     *
-     * <p>The arrays must be sorted ascending and monotonically increasing.
-     *
-     * @param fromSp array of dimensions in SP
-     * @param toDp array of dimensions in DP that correspond to an SP value in fromSp
-     *
-     * @throws IllegalArgumentException if the array lengths don't match or are empty
-     * @hide
+     * Converts a dimension in "dp" back to "sp".
      */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public FontScaleConverter(@NonNull float[] fromSp, @NonNull float[] toDp) {
-        if (fromSp.length != toDp.length || fromSp.length == 0) {
-            throw new IllegalArgumentException("Array lengths must match and be nonzero");
-        }
-
-        mFromSpValues = fromSp;
-        mToDpValues = toDp;
-    }
-
-    /**
-     * Convert a dimension in "dp" back to "sp" using the lookup table.
-     *
-     * @hide
-     */
-    public float convertDpToSp(float dp) {
-        return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
-    }
-
-    /**
-     * Convert a dimension in "sp" to "dp" using the lookup table.
-     *
-     * @hide
-     */
-    public float convertSpToDp(float sp) {
-        return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
-    }
-
-    private static float lookupAndInterpolate(
-            float sourceValue,
-            float[] sourceValues,
-            float[] targetValues
-    ) {
-        final float sourceValuePositive = Math.abs(sourceValue);
-        // TODO(b/247861374): find a match at a higher index?
-        final float sign = Math.signum(sourceValue);
-        // We search for exact matches only, even if it's just a little off. The interpolation will
-        // handle any non-exact matches.
-        final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
-        if (index >= 0) {
-            // exact match, return the matching dp
-            return sign * targetValues[index];
-        } else {
-            // must be a value in between index and index + 1: interpolate.
-            final int lowerIndex = -(index + 1) - 1;
-
-            final float startSp;
-            final float endSp;
-            final float startDp;
-            final float endDp;
-
-            if (lowerIndex >= sourceValues.length - 1) {
-                // It's past our lookup table. Determine the last elements' scaling factor and use.
-                startSp = sourceValues[sourceValues.length - 1];
-                startDp = targetValues[sourceValues.length - 1];
-
-                if (startSp == 0) return 0;
-
-                final float scalingFactor = startDp / startSp;
-                return sourceValue * scalingFactor;
-            } else if (lowerIndex == -1) {
-                // It's smaller than the smallest value in our table. Interpolate from 0.
-                startSp = 0;
-                startDp = 0;
-                endSp = sourceValues[0];
-                endDp = targetValues[0];
-            } else {
-                startSp = sourceValues[lowerIndex];
-                endSp = sourceValues[lowerIndex + 1];
-                startDp = targetValues[lowerIndex];
-                endDp = targetValues[lowerIndex + 1];
-            }
-
-            return sign
-                    * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive);
-        }
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null) return false;
-        if (!(o instanceof FontScaleConverter)) return false;
-        FontScaleConverter that = (FontScaleConverter) o;
-        return Arrays.equals(mFromSpValues, that.mFromSpValues)
-                && Arrays.equals(mToDpValues, that.mToDpValues);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = Arrays.hashCode(mFromSpValues);
-        result = 31 * result + Arrays.hashCode(mToDpValues);
-        return result;
-    }
-
-    @Override
-    public String toString() {
-        return "FontScaleConverter{"
-                + "fromSpValues="
-                + Arrays.toString(mFromSpValues)
-                + ", toDpValues="
-                + Arrays.toString(mToDpValues)
-                + '}';
-    }
+    float convertDpToSp(float dp);
 }
diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java
index 5eb6526..5d31cc0 100644
--- a/core/java/android/content/res/FontScaleConverterFactory.java
+++ b/core/java/android/content/res/FontScaleConverterFactory.java
@@ -16,6 +16,8 @@
 
 package android.content.res;
 
+import android.annotation.AnyThread;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.MathUtils;
@@ -24,67 +26,88 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 /**
- * Stores lookup tables for creating {@link FontScaleConverter}s at various scales.
+ * Creates {@link FontScaleConverter}s at various scales.
  *
- * @hide
+ * Generally you shouldn't need this; you can use {@link
+ * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do the
+ * scaling conversion for you. But for UI frameworks or other situations where you need to do the
+ * conversion without an Android Context, you can use this class.
  */
+@FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
 public class FontScaleConverterFactory {
     private static final float SCALE_KEY_MULTIPLIER = 100f;
 
+    /** @hide */
+    // GuardedBy("LOOKUP_TABLES_WRITE_LOCK") but only for writes!
     @VisibleForTesting
-    static final SparseArray<FontScaleConverter> LOOKUP_TABLES = new SparseArray<>();
+    @NonNull
+    public static volatile SparseArray<FontScaleConverter> sLookupTables = new SparseArray<>();
+
+    /**
+     * This is a write lock only! We don't care about synchronization on reads; they can be a bit
+     * out of date. But all writes have to be atomic, so we use this similar to a
+     * CopyOnWriteArrayList.
+     */
+    private static final Object LOOKUP_TABLES_WRITE_LOCK = new Object();
 
     private static float sMinScaleBeforeCurvesApplied = 1.05f;
 
     static {
         // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and
         // manually tweaked for optimum readability.
-        put(
-                /* scaleKey= */ 1.15f,
-                new FontScaleConverter(
-                        /* fromSp= */
-                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
-                        /* toDp=   */
-                        new float[] { 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f,   30f,  100})
-        );
+        synchronized (LOOKUP_TABLES_WRITE_LOCK) {
+            putInto(
+                    sLookupTables,
+                    /* scaleKey= */ 1.15f,
+                    new FontScaleConverterImpl(
+                            /* fromSp= */
+                            new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
+                            /* toDp=   */
+                            new float[] { 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f,   30f,  100})
+            );
 
-        put(
-                /* scaleKey= */ 1.3f,
-                new FontScaleConverter(
-                        /* fromSp= */
-                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
-                        /* toDp=   */
-                        new float[] {10.4f,   13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f,   30f,  100})
-        );
+            putInto(
+                    sLookupTables,
+                    /* scaleKey= */ 1.3f,
+                    new FontScaleConverterImpl(
+                            /* fromSp= */
+                            new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
+                            /* toDp=   */
+                            new float[] {10.4f,   13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f,   30f,  100})
+            );
 
-        put(
-                /* scaleKey= */ 1.5f,
-                new FontScaleConverter(
-                        /* fromSp= */
-                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
-                        /* toDp=   */
-                        new float[] {  12f,   15f,   18f,   22f,   24f,   26f,   28f,   30f,  100})
-        );
+            putInto(
+                    sLookupTables,
+                    /* scaleKey= */ 1.5f,
+                    new FontScaleConverterImpl(
+                            /* fromSp= */
+                            new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
+                            /* toDp=   */
+                            new float[] {  12f,   15f,   18f,   22f,   24f,   26f,   28f,   30f,  100})
+            );
 
-        put(
-                /* scaleKey= */ 1.8f,
-                new FontScaleConverter(
-                        /* fromSp= */
-                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
-                        /* toDp=   */
-                        new float[] {14.4f,   18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f,  100})
-        );
+            putInto(
+                    sLookupTables,
+                    /* scaleKey= */ 1.8f,
+                    new FontScaleConverterImpl(
+                            /* fromSp= */
+                            new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
+                            /* toDp=   */
+                            new float[] {14.4f,   18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f,  100})
+            );
 
-        put(
-                /* scaleKey= */ 2f,
-                new FontScaleConverter(
-                        /* fromSp= */
-                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
-                        /* toDp=   */
-                        new float[] {  16f,   20f,   24f,   26f,   30f,   34f,   36f,   38f,  100})
-        );
+            putInto(
+                    sLookupTables,
+                    /* scaleKey= */ 2f,
+                    new FontScaleConverterImpl(
+                            /* fromSp= */
+                            new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
+                            /* toDp=   */
+                            new float[] {  16f,   20f,   24f,   26f,   30f,   34f,   36f,   38f,  100})
+            );
+        }
 
-        sMinScaleBeforeCurvesApplied = getScaleFromKey(LOOKUP_TABLES.keyAt(0)) - 0.02f;
+        sMinScaleBeforeCurvesApplied = getScaleFromKey(sLookupTables.keyAt(0)) - 0.02f;
         if (sMinScaleBeforeCurvesApplied <= 1.0f) {
             throw new IllegalStateException(
                     "You should only apply non-linear scaling to font scales > 1"
@@ -100,9 +123,9 @@
      *
      * <p>Example usage:
      * <code>isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code>
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
+    @AnyThread
     public static boolean isNonLinearFontScalingActive(float fontScale) {
         return fontScale >= sMinScaleBeforeCurvesApplied;
     }
@@ -113,10 +136,10 @@
      * @param fontScale the scale factor, usually from {@link Configuration#fontScale}.
      *
      * @return a converter for the given scale, or null if non-linear scaling should not be used.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
     @Nullable
+    @AnyThread
     public static FontScaleConverter forScale(float fontScale) {
         if (!isNonLinearFontScalingActive(fontScale)) {
             return null;
@@ -128,23 +151,33 @@
         }
 
         // Didn't find an exact match: interpolate between two existing tables
-        final int index = LOOKUP_TABLES.indexOfKey(getKey(fontScale));
+        final int index = sLookupTables.indexOfKey(getKey(fontScale));
         if (index >= 0) {
             // This should never happen, should have been covered by get() above.
-            return LOOKUP_TABLES.valueAt(index);
+            return sLookupTables.valueAt(index);
         }
         // Didn't find an exact match: interpolate between two existing tables
         final int lowerIndex = -(index + 1) - 1;
         final int higherIndex = lowerIndex + 1;
-        if (lowerIndex < 0 || higherIndex >= LOOKUP_TABLES.size()) {
+        if (lowerIndex < 0 || higherIndex >= sLookupTables.size()) {
             // We have gone beyond our bounds and have nothing to interpolate between. Just give
             // them a straight linear table instead.
             // This works because when FontScaleConverter encounters a size beyond its bounds, it
             // calculates a linear fontScale factor using the ratio of the last element pair.
-            return new FontScaleConverter(new float[] {1f}, new float[] {fontScale});
+            FontScaleConverterImpl converter = new FontScaleConverterImpl(
+                    new float[]{1f},
+                    new float[]{fontScale}
+            );
+
+            if (Flags.fontScaleConverterPublic()) {
+                // Cache for next time.
+                put(fontScale, converter);
+            }
+
+            return converter;
         } else {
-            float startScale = getScaleFromKey(LOOKUP_TABLES.keyAt(lowerIndex));
-            float endScale = getScaleFromKey(LOOKUP_TABLES.keyAt(higherIndex));
+            float startScale = getScaleFromKey(sLookupTables.keyAt(lowerIndex));
+            float endScale = getScaleFromKey(sLookupTables.keyAt(higherIndex));
             float interpolationPoint = MathUtils.constrainedMap(
                     /* rangeMin= */ 0f,
                     /* rangeMax= */ 1f,
@@ -152,10 +185,18 @@
                     endScale,
                     fontScale
             );
-            return createInterpolatedTableBetween(
-                    LOOKUP_TABLES.valueAt(lowerIndex),
-                    LOOKUP_TABLES.valueAt(higherIndex),
-                    interpolationPoint);
+            FontScaleConverter converter = createInterpolatedTableBetween(
+                    sLookupTables.valueAt(lowerIndex),
+                    sLookupTables.valueAt(higherIndex),
+                    interpolationPoint
+            );
+
+            if (Flags.fontScaleConverterPublic()) {
+                // Cache for next time.
+                put(fontScale, converter);
+            }
+
+            return converter;
         }
     }
 
@@ -175,7 +216,7 @@
             dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint);
         }
 
-        return new FontScaleConverter(commonSpSizes, dpInterpolated);
+        return new FontScaleConverterImpl(commonSpSizes, dpInterpolated);
     }
 
     private static int getKey(float fontScale) {
@@ -187,11 +228,24 @@
     }
 
     private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) {
-        LOOKUP_TABLES.put(getKey(scaleKey), fontScaleConverter);
+        // Dollar-store CopyOnWriteSparseArray, since this is the only write op we need.
+        synchronized (LOOKUP_TABLES_WRITE_LOCK) {
+            var newTable = sLookupTables.clone();
+            putInto(newTable, scaleKey, fontScaleConverter);
+            sLookupTables = newTable;
+        }
+    }
+
+    private static void putInto(
+            SparseArray<FontScaleConverter> table,
+            float scaleKey,
+            @NonNull FontScaleConverter fontScaleConverter
+    ) {
+        table.put(getKey(scaleKey), fontScaleConverter);
     }
 
     @Nullable
     private static FontScaleConverter get(float scaleKey) {
-        return LOOKUP_TABLES.get(getKey(scaleKey));
+        return sLookupTables.get(getKey(scaleKey));
     }
 }
diff --git a/core/java/android/content/res/FontScaleConverterImpl.java b/core/java/android/content/res/FontScaleConverterImpl.java
new file mode 100644
index 0000000..1968c4e
--- /dev/null
+++ b/core/java/android/content/res/FontScaleConverterImpl.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import android.annotation.NonNull;
+import android.util.MathUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+
+/**
+ * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
+ * "dp" dimension according to a non-linear curve by interpolating values in a lookup table.
+ *
+ * {@see FontScaleConverter}
+ *
+ * @hide
+ */
+// Needs to be public so the Kotlin test can see it
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FontScaleConverterImpl implements FontScaleConverter {
+
+    /** @hide */
+    @VisibleForTesting
+    public final float[] mFromSpValues;
+    /** @hide */
+    @VisibleForTesting
+    public final float[] mToDpValues;
+
+    /**
+     * Creates a lookup table for the given conversions.
+     *
+     * <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
+     *
+     * <p>The arrays must be sorted ascending and monotonically increasing.
+     *
+     * @param fromSp array of dimensions in SP
+     * @param toDp array of dimensions in DP that correspond to an SP value in fromSp
+     *
+     * @throws IllegalArgumentException if the array lengths don't match or are empty
+     * @hide
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public FontScaleConverterImpl(@NonNull float[] fromSp, @NonNull float[] toDp) {
+        if (fromSp.length != toDp.length || fromSp.length == 0) {
+            throw new IllegalArgumentException("Array lengths must match and be nonzero");
+        }
+
+        mFromSpValues = fromSp;
+        mToDpValues = toDp;
+    }
+
+    /**
+     * Convert a dimension in "dp" back to "sp" using the lookup table.
+     *
+     * @hide
+     */
+    @Override
+    public float convertDpToSp(float dp) {
+        return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
+    }
+
+    /**
+     * Convert a dimension in "sp" to "dp" using the lookup table.
+     *
+     * @hide
+     */
+    @Override
+    public float convertSpToDp(float sp) {
+        return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
+    }
+
+    private static float lookupAndInterpolate(
+            float sourceValue,
+            float[] sourceValues,
+            float[] targetValues
+    ) {
+        final float sourceValuePositive = Math.abs(sourceValue);
+        // TODO(b/247861374): find a match at a higher index?
+        final float sign = Math.signum(sourceValue);
+        // We search for exact matches only, even if it's just a little off. The interpolation will
+        // handle any non-exact matches.
+        final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
+        if (index >= 0) {
+            // exact match, return the matching dp
+            return sign * targetValues[index];
+        } else {
+            // must be a value in between index and index + 1: interpolate.
+            final int lowerIndex = -(index + 1) - 1;
+
+            final float startSp;
+            final float endSp;
+            final float startDp;
+            final float endDp;
+
+            if (lowerIndex >= sourceValues.length - 1) {
+                // It's past our lookup table. Determine the last elements' scaling factor and use.
+                startSp = sourceValues[sourceValues.length - 1];
+                startDp = targetValues[sourceValues.length - 1];
+
+                if (startSp == 0) return 0;
+
+                final float scalingFactor = startDp / startSp;
+                return sourceValue * scalingFactor;
+            } else if (lowerIndex == -1) {
+                // It's smaller than the smallest value in our table. Interpolate from 0.
+                startSp = 0;
+                startDp = 0;
+                endSp = sourceValues[0];
+                endDp = targetValues[0];
+            } else {
+                startSp = sourceValues[lowerIndex];
+                endSp = sourceValues[lowerIndex + 1];
+                startDp = targetValues[lowerIndex];
+                endDp = targetValues[lowerIndex + 1];
+            }
+
+            return sign
+                    * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null) return false;
+        if (!(o instanceof FontScaleConverterImpl)) return false;
+        FontScaleConverterImpl that = (FontScaleConverterImpl) o;
+        return Arrays.equals(mFromSpValues, that.mFromSpValues)
+                && Arrays.equals(mToDpValues, that.mToDpValues);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Arrays.hashCode(mFromSpValues);
+        result = 31 * result + Arrays.hashCode(mToDpValues);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "FontScaleConverter{"
+                + "fromSpValues="
+                + Arrays.toString(mFromSpValues)
+                + ", toDpValues="
+                + Arrays.toString(mToDpValues)
+                + '}';
+    }
+}
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index 3a00d91..db81e84 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -10,6 +10,15 @@
 }
 
 flag {
+    name: "font_scale_converter_public"
+    namespace: "accessibility"
+    description: "Enables the public API for FontScaleConverter, including enabling thread-safe caching."
+    bug: "239736383"
+    # fixed_read_only or device wont boot because of permission issues accessing flags during boot
+    is_fixed_read_only: true
+}
+
+flag {
     name: "asset_file_descriptor_frro"
     namespace: "resource_manager"
     description: "Feature flag for passing in an AssetFileDescriptor to create an frro"
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index 52bba14..870546a 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -22,14 +22,8 @@
 import android.content.res.Resources;
 import android.database.sqlite.SQLiteClosable;
 import android.database.sqlite.SQLiteException;
-import android.os.Binder;
-import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.Process;
-import android.util.Log;
-import android.util.LongSparseArray;
-import android.util.SparseIntArray;
 
 import dalvik.annotation.optimization.FastNative;
 import dalvik.system.CloseGuard;
@@ -44,6 +38,9 @@
  * consumer for reading.
  * </p>
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+        "com.android.hoststubgen.nativesubstitution.CursorWindow_host")
 public class CursorWindow extends SQLiteClosable implements Parcelable {
     private static final String STATS_TAG = "CursorWindowStats";
 
@@ -61,7 +58,7 @@
     private int mStartPos;
     private final String mName;
 
-    private final CloseGuard mCloseGuard = CloseGuard.get();
+    private final CloseGuard mCloseGuard;
 
     // May throw CursorWindowAllocationException
     private static native long nativeCreate(String name, int cursorWindowSize);
@@ -147,7 +144,7 @@
         if (mWindowPtr == 0) {
             throw new AssertionError(); // Not possible, the native code won't return it.
         }
-        mCloseGuard.open("CursorWindow.close");
+        mCloseGuard = createCloseGuard();
     }
 
     /**
@@ -175,7 +172,18 @@
             throw new AssertionError(); // Not possible, the native code won't return it.
         }
         mName = nativeGetName(mWindowPtr);
-        mCloseGuard.open("CursorWindow.close");
+        mCloseGuard = createCloseGuard();
+    }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    private CloseGuard createCloseGuard() {
+        final CloseGuard closeGuard = CloseGuard.get();
+        closeGuard.open("CursorWindow.close");
+        return closeGuard;
+    }
+
+    private CloseGuard createCloseGuard$ravenwood() {
+        return null;
     }
 
     @Override
@@ -749,6 +757,7 @@
         dispose();
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
     private static int getCursorWindowSize() {
         if (sCursorWindowSize < 0) {
             // The cursor window size. resource xml file specifies the value in kB.
@@ -759,6 +768,10 @@
         return sCursorWindowSize;
     }
 
+    private static int getCursorWindowSize$ravenwood() {
+        return 1024;
+    }
+
     @Override
     public String toString() {
         return getName() + " {" + Long.toHexString(mWindowPtr) + "}";
diff --git a/core/java/android/database/SQLException.java b/core/java/android/database/SQLException.java
index 3402026..4026f20 100644
--- a/core/java/android/database/SQLException.java
+++ b/core/java/android/database/SQLException.java
@@ -19,6 +19,7 @@
 /**
  * An exception that indicates there was an error with SQL parsing or execution.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class SQLException extends RuntimeException {
     public SQLException() {
     }
diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java
index 2fca729..8eb512a 100644
--- a/core/java/android/database/sqlite/SQLiteClosable.java
+++ b/core/java/android/database/sqlite/SQLiteClosable.java
@@ -25,6 +25,7 @@
  *
  * This class implements a primitive reference counting scheme for database objects.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public abstract class SQLiteClosable implements Closeable {
     @UnsupportedAppUsage
     private int mReferenceCount = 1;
diff --git a/core/java/android/database/sqlite/SQLiteException.java b/core/java/android/database/sqlite/SQLiteException.java
index a1d9c9f..531b40a 100644
--- a/core/java/android/database/sqlite/SQLiteException.java
+++ b/core/java/android/database/sqlite/SQLiteException.java
@@ -21,6 +21,7 @@
 /**
  * A SQLite exception that indicates there was an error with SQL parsing or execution.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class SQLiteException extends SQLException {
     public SQLiteException() {
     }
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 3ab889d..665d8d2 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -557,13 +557,15 @@
      * on a particular SessionConfiguration.</p>
      *
      * @return List of CameraCharacteristic keys containing characterisitics specific to a session
-     * configuration. For Android 15, this list only contains CONTROL_ZOOM_RATIO_RANGE.
+     * configuration. For Android 15, this list only contains CONTROL_ZOOM_RATIO_RANGE and
+     * SCALER_AVAILABLE_MAX_DIGITAL_ZOOM.
      */
     @NonNull
     @FlaggedApi(Flags.FLAG_FEATURE_COMBINATION_QUERY)
     public List<CameraCharacteristics.Key<?>> getAvailableSessionCharacteristicsKeys() {
         if (mAvailableSessionCharacteristicsKeys == null) {
-            mAvailableSessionCharacteristicsKeys = Arrays.asList(CONTROL_ZOOM_RATIO_RANGE);
+            mAvailableSessionCharacteristicsKeys =
+                    Arrays.asList(CONTROL_ZOOM_RATIO_RANGE, SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
         }
         return mAvailableSessionCharacteristicsKeys;
     }
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index f71e853..ffd7212 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -715,6 +715,14 @@
         boolean startOffload();
 
         void stopOffload();
+
+        /**
+         * Called when {@link DisplayOffloadSession} tries to block screen turning on.
+         *
+         * @param unblocker a {@link Runnable} executed upon all work required before screen turning
+         *                  on is done.
+         */
+        void onBlockingScreenOn(Runnable unblocker);
     }
 
     /** A session token that associates a internal display with a {@link DisplayOffloader}. */
@@ -734,6 +742,15 @@
          */
         void updateBrightness(float brightness);
 
+        /**
+         * Called while display is turning to state ON to leave a small period for displayoffload
+         * session to finish some work.
+         *
+         * @param unblocker a {@link Runnable} used by displayoffload session to notify
+         *                  {@link DisplayManager} that it can continue turning screen on.
+         */
+        boolean blockScreenOn(Runnable unblocker);
+
         /** Returns whether displayoffload supports the given display state. */
         static boolean isSupportedOffloadState(int displayState) {
             return Display.isSuspendedState(displayState);
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/core/java/android/hardware/face/FaceSensorConfigurations.aidl
similarity index 74%
rename from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
rename to core/java/android/hardware/face/FaceSensorConfigurations.aidl
index 6c1f0fc..26367b3 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/core/java/android/hardware/face/FaceSensorConfigurations.aidl
@@ -13,12 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.hardware.face;
 
-package android.companion.virtual.camera;
-
-/**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
- */
-parcelable VirtualCameraMetadata;
+parcelable FaceSensorConfigurations;
\ No newline at end of file
diff --git a/core/java/android/hardware/face/FaceSensorConfigurations.java b/core/java/android/hardware/face/FaceSensorConfigurations.java
new file mode 100644
index 0000000..6ef692f
--- /dev/null
+++ b/core/java/android/hardware/face/FaceSensorConfigurations.java
@@ -0,0 +1,182 @@
+/*
+ * 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.hardware.face;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.SensorProps;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Provides the sensor props for face sensor, if available.
+ * @hide
+ */
+public class FaceSensorConfigurations implements Parcelable {
+    private static final String TAG = "FaceSensorConfigurations";
+
+    private final boolean mResetLockoutRequiresChallenge;
+    private final Map<String, SensorProps[]> mSensorPropsMap;
+
+    public static final Creator<FaceSensorConfigurations> CREATOR =
+            new Creator<FaceSensorConfigurations>() {
+                @Override
+                public FaceSensorConfigurations createFromParcel(Parcel in) {
+                    return new FaceSensorConfigurations(in);
+                }
+
+                @Override
+                public FaceSensorConfigurations[] newArray(int size) {
+                    return new FaceSensorConfigurations[size];
+                }
+            };
+
+    public FaceSensorConfigurations(boolean resetLockoutRequiresChallenge) {
+        mResetLockoutRequiresChallenge = resetLockoutRequiresChallenge;
+        mSensorPropsMap = new HashMap<>();
+    }
+
+    protected FaceSensorConfigurations(Parcel in) {
+        mResetLockoutRequiresChallenge = in.readByte() != 0;
+        mSensorPropsMap = in.readHashMap(null, String.class, SensorProps[].class);
+    }
+
+    /**
+     * Process AIDL instances to extract sensor props and add it to the sensor map.
+     * @param aidlInstances available face AIDL instances
+     * @param getIFace function that provides the daemon for the specific instance
+     */
+    public void addAidlConfigs(@NonNull String[] aidlInstances,
+            @NonNull Function<String, IFace> getIFace) {
+        for (String aidlInstance : aidlInstances) {
+            final String fqName = IFace.DESCRIPTOR + "/" + aidlInstance;
+            IFace face = getIFace.apply(fqName);
+            try {
+                if (face != null) {
+                    mSensorPropsMap.put(aidlInstance, face.getSensorProps());
+                } else {
+                    Slog.e(TAG, "Unable to get declared service: " + fqName);
+                }
+            } catch (RemoteException e) {
+                Log.d(TAG, "Unable to get sensor properties!");
+            }
+        }
+    }
+
+    /**
+     * Parse through HIDL configuration and add it to the sensor map.
+     */
+    public void addHidlConfigs(@NonNull String[] hidlConfigStrings,
+            @NonNull Context context) {
+        final List<HidlFaceSensorConfig> hidlFaceSensorConfigs = new ArrayList<>();
+        for (String hidlConfig: hidlConfigStrings) {
+            final HidlFaceSensorConfig hidlFaceSensorConfig = new HidlFaceSensorConfig();
+            try {
+                hidlFaceSensorConfig.parse(hidlConfig, context);
+            } catch (Exception e) {
+                Log.e(TAG, "HIDL sensor configuration format is incorrect.");
+                continue;
+            }
+            if (hidlFaceSensorConfig.getModality() == TYPE_FACE) {
+                hidlFaceSensorConfigs.add(hidlFaceSensorConfig);
+            }
+        }
+        final String hidlHalInstanceName = "defaultHIDL";
+        mSensorPropsMap.put(hidlHalInstanceName, hidlFaceSensorConfigs.toArray(
+                new SensorProps[hidlFaceSensorConfigs.size()]));
+    }
+
+    /**
+     * Returns true if any face sensors have been added.
+     */
+    public boolean hasSensorConfigurations() {
+        return mSensorPropsMap.size() > 0;
+    }
+
+    /**
+     * Returns true if there is only a single face sensor configuration available.
+     */
+    public boolean isSingleSensorConfigurationPresent() {
+        return mSensorPropsMap.size() == 1;
+    }
+
+    /**
+     * Return sensor props for the given instance. If instance is not available,
+     * then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairForInstance(String instance) {
+        if (mSensorPropsMap.containsKey(instance)) {
+            return new Pair<>(instance, mSensorPropsMap.get(instance));
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the first pair of instance and sensor props, which does not correspond to the given
+     * If instance is not available, then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairNotForInstance(String instance) {
+        Optional<String> notAVirtualInstance = mSensorPropsMap.keySet().stream().filter(
+                (instanceName) -> !instanceName.equals(instance)).findFirst();
+        return notAVirtualInstance.map(this::getSensorPairForInstance).orElseGet(
+                this::getSensorPair);
+    }
+
+    /**
+     * Returns the first pair of instance and sensor props that has been added to the map.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPair() {
+        Optional<String> optionalInstance = mSensorPropsMap.keySet().stream().findFirst();
+        return optionalInstance.map(this::getSensorPairForInstance).orElse(null);
+
+    }
+
+    public boolean getResetLockoutRequiresChallenge() {
+        return mResetLockoutRequiresChallenge;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByte((byte) (mResetLockoutRequiresChallenge ? 1 : 0));
+        dest.writeMap(mSensorPropsMap);
+    }
+}
diff --git a/core/java/android/hardware/face/HidlFaceSensorConfig.java b/core/java/android/hardware/face/HidlFaceSensorConfig.java
new file mode 100644
index 0000000..cab146d
--- /dev/null
+++ b/core/java/android/hardware/face/HidlFaceSensorConfig.java
@@ -0,0 +1,85 @@
+/*
+ * 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.hardware.face;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.common.SensorStrength;
+import android.hardware.biometrics.face.SensorProps;
+
+import com.android.internal.R;
+
+/**
+ * Parse HIDL face sensor config and map it to SensorProps.aidl to match AIDL.
+ * See core/res/res/values/config.xml config_biometric_sensors
+ * @hide
+ */
+public final class HidlFaceSensorConfig extends SensorProps {
+    private int mSensorId;
+    private int mModality;
+    private int mStrength;
+
+    /**
+     * Parse through the config string and map it to SensorProps.aidl.
+     * @throws IllegalArgumentException when config string has unexpected format
+     */
+    public void parse(@NonNull String config, @NonNull Context context)
+            throws IllegalArgumentException {
+        final String[] elems = config.split(":");
+        if (elems.length < 3) {
+            throw new IllegalArgumentException();
+        }
+        mSensorId = Integer.parseInt(elems[0]);
+        mModality = Integer.parseInt(elems[1]);
+        mStrength = Integer.parseInt(elems[2]);
+        mapHidlToAidlFaceSensorConfigurations(context);
+    }
+
+    @BiometricAuthenticator.Modality
+    public int getModality() {
+        return mModality;
+    }
+
+    private void mapHidlToAidlFaceSensorConfigurations(@NonNull Context context) {
+        commonProps = new CommonProps();
+        commonProps.sensorId = mSensorId;
+        commonProps.sensorStrength = authenticatorStrengthToPropertyStrength(mStrength);
+        halControlsPreview = context.getResources().getBoolean(
+                R.bool.config_faceAuthSupportsSelfIllumination);
+        commonProps.maxEnrollmentsPerUser = context.getResources().getInteger(
+                R.integer.config_faceMaxTemplatesPerUser);
+        commonProps.componentInfo = null;
+        supportsDetectInteraction = false;
+    }
+
+    private byte authenticatorStrengthToPropertyStrength(
+            @BiometricManager.Authenticators.Types int strength) {
+        switch (strength) {
+            case BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE:
+                return SensorStrength.CONVENIENCE;
+            case BiometricManager.Authenticators.BIOMETRIC_WEAK:
+                return SensorStrength.WEAK;
+            case BiometricManager.Authenticators.BIOMETRIC_STRONG:
+                return SensorStrength.STRONG;
+            default:
+                throw new IllegalArgumentException("Unknown strength: " + strength);
+        }
+    }
+}
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 7080133..0096877 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -26,6 +26,7 @@
 import android.hardware.face.Face;
 import android.hardware.face.FaceAuthenticateOptions;
 import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.FaceSensorConfigurations;
 import android.view.Surface;
 
 /**
@@ -167,6 +168,10 @@
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerAuthenticators(in List<FaceSensorPropertiesInternal> hidlSensors);
 
+    //Register all available face sensors.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void registerAuthenticatorsLegacy(in FaceSensorConfigurations faceSensorConfigurations);
+
     // Adds a callback which gets called when the service registers all of the face
     // authenticators. The callback is automatically removed after it's invoked.
     void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback);
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl
similarity index 74%
copy from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
copy to core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl
index 6c1f0fc..ebb05dc 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.aidl
@@ -13,12 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.hardware.fingerprint;
 
-package android.companion.virtual.camera;
-
-/**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
- */
-parcelable VirtualCameraMetadata;
+parcelable FingerprintSensorConfigurations;
\ No newline at end of file
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
new file mode 100644
index 0000000..f214494a
--- /dev/null
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
@@ -0,0 +1,184 @@
+/*
+ * 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.hardware.fingerprint;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Provides the sensor props for fingerprint sensor, if available.
+ * @hide
+ */
+
+public class FingerprintSensorConfigurations implements Parcelable {
+    private static final String TAG = "FingerprintSensorConfigurations";
+
+    private final Map<String, SensorProps[]> mSensorPropsMap;
+    private final boolean mResetLockoutRequiresHardwareAuthToken;
+
+    public static final Creator<FingerprintSensorConfigurations> CREATOR =
+            new Creator<>() {
+                @Override
+                public FingerprintSensorConfigurations createFromParcel(Parcel in) {
+                    return new FingerprintSensorConfigurations(in);
+                }
+
+                @Override
+                public FingerprintSensorConfigurations[] newArray(int size) {
+                    return new FingerprintSensorConfigurations[size];
+                }
+            };
+
+    public FingerprintSensorConfigurations(boolean resetLockoutRequiresHardwareAuthToken) {
+        mResetLockoutRequiresHardwareAuthToken = resetLockoutRequiresHardwareAuthToken;
+        mSensorPropsMap = new HashMap<>();
+    }
+
+    /**
+     * Process AIDL instances to extract sensor props and add it to the sensor map.
+     * @param aidlInstances available face AIDL instances
+     * @param getIFingerprint function that provides the daemon for the specific instance
+     */
+    public void addAidlSensors(@NonNull String[] aidlInstances,
+            @NonNull Function<String, IFingerprint> getIFingerprint) {
+        for (String aidlInstance : aidlInstances) {
+            try {
+                final String fqName = IFingerprint.DESCRIPTOR + "/" + aidlInstance;
+                final IFingerprint fp = getIFingerprint.apply(fqName);
+                if (fp != null) {
+                    SensorProps[] props = fp.getSensorProps();
+                    mSensorPropsMap.put(aidlInstance, props);
+                } else {
+                    Log.d(TAG, "IFingerprint null for instance " + aidlInstance);
+                }
+            } catch (RemoteException e) {
+                Log.d(TAG, "Unable to get sensor properties!");
+            }
+        }
+    }
+
+    /**
+     * Parse through HIDL configuration and add it to the sensor map.
+     */
+    public void addHidlSensors(@NonNull String[] hidlConfigStrings,
+            @NonNull Context context) {
+        final List<HidlFingerprintSensorConfig> hidlFingerprintSensorConfigs = new ArrayList<>();
+        for (String hidlConfigString : hidlConfigStrings) {
+            final HidlFingerprintSensorConfig hidlFingerprintSensorConfig =
+                    new HidlFingerprintSensorConfig();
+            try {
+                hidlFingerprintSensorConfig.parse(hidlConfigString, context);
+            } catch (Exception e) {
+                Log.e(TAG, "HIDL sensor configuration format is incorrect.");
+                continue;
+            }
+            if (hidlFingerprintSensorConfig.getModality() == TYPE_FINGERPRINT) {
+                hidlFingerprintSensorConfigs.add(hidlFingerprintSensorConfig);
+            }
+        }
+        final String hidlHalInstanceName = "defaultHIDL";
+        mSensorPropsMap.put(hidlHalInstanceName,
+                hidlFingerprintSensorConfigs.toArray(
+                        new HidlFingerprintSensorConfig[hidlFingerprintSensorConfigs.size()]));
+    }
+
+    protected FingerprintSensorConfigurations(Parcel in) {
+        mResetLockoutRequiresHardwareAuthToken = in.readByte() != 0;
+        mSensorPropsMap = in.readHashMap(null /* loader */, String.class, SensorProps[].class);
+    }
+
+    /**
+     * Returns true if any fingerprint sensors have been added.
+     */
+    public boolean hasSensorConfigurations() {
+        return mSensorPropsMap.size() > 0;
+    }
+
+    /**
+     * Returns true if there is only a single fingerprint sensor configuration available.
+     */
+    public boolean isSingleSensorConfigurationPresent() {
+        return mSensorPropsMap.size() == 1;
+    }
+
+    /**
+     * Return sensor props for the given instance. If instance is not available,
+     * then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairForInstance(String instance) {
+        if (mSensorPropsMap.containsKey(instance)) {
+            return new Pair<>(instance, mSensorPropsMap.get(instance));
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the first pair of instance and sensor props, which does not correspond to the given
+     * If instance is not available, then null is returned.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPairNotForInstance(String instance) {
+        Optional<String> notAVirtualInstance = mSensorPropsMap.keySet().stream().filter(
+                (instanceName) -> !instanceName.equals(instance)).findFirst();
+        return notAVirtualInstance.map(this::getSensorPairForInstance).orElseGet(
+                this::getSensorPair);
+    }
+
+    /**
+     * Returns the first pair of instance and sensor props that has been added to the map.
+     */
+    @Nullable
+    public Pair<String, SensorProps[]> getSensorPair() {
+        Optional<String> optionalInstance = mSensorPropsMap.keySet().stream().findFirst();
+        return optionalInstance.map(this::getSensorPairForInstance).orElse(null);
+
+    }
+
+    public boolean getResetLockoutRequiresHardwareAuthToken() {
+        return mResetLockoutRequiresHardwareAuthToken;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeByte((byte) (mResetLockoutRequiresHardwareAuthToken ? 1 : 0));
+        dest.writeMap(mSensorPropsMap);
+    }
+}
diff --git a/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java b/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java
new file mode 100644
index 0000000..d481153
--- /dev/null
+++ b/core/java/android/hardware/fingerprint/HidlFingerprintSensorConfig.java
@@ -0,0 +1,119 @@
+/*
+ * 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.hardware.fingerprint;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.common.SensorStrength;
+import android.hardware.biometrics.fingerprint.FingerprintSensorType;
+import android.hardware.biometrics.fingerprint.SensorLocation;
+import android.hardware.biometrics.fingerprint.SensorProps;
+
+import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * Parse HIDL fingerprint sensor config and map it to SensorProps.aidl to match AIDL.
+ * See core/res/res/values/config.xml config_biometric_sensors
+ * @hide
+ */
+public final class HidlFingerprintSensorConfig extends SensorProps {
+    private int mSensorId;
+    private int mModality;
+    private int mStrength;
+
+    /**
+     * Parse through the config string and map it to SensorProps.aidl.
+     * @throws IllegalArgumentException when config string has unexpected format
+     */
+    public void parse(@NonNull String config, @NonNull Context context)
+            throws IllegalArgumentException {
+        final String[] elems = config.split(":");
+        if (elems.length < 3) {
+            throw new IllegalArgumentException();
+        }
+        mSensorId = Integer.parseInt(elems[0]);
+        mModality = Integer.parseInt(elems[1]);
+        mStrength = Integer.parseInt(elems[2]);
+        mapHidlToAidlSensorConfiguration(context);
+    }
+
+    @BiometricAuthenticator.Modality
+    public int getModality() {
+        return mModality;
+    }
+
+    private void mapHidlToAidlSensorConfiguration(@NonNull Context context) {
+        commonProps = new CommonProps();
+        commonProps.componentInfo = null;
+        commonProps.sensorId = mSensorId;
+        commonProps.sensorStrength = authenticatorStrengthToPropertyStrength(mStrength);
+        commonProps.maxEnrollmentsPerUser = context.getResources().getInteger(
+                R.integer.config_fingerprintMaxTemplatesPerUser);
+        halControlsIllumination = false;
+        sensorLocations = new SensorLocation[1];
+
+        final int[] udfpsProps = context.getResources().getIntArray(
+                com.android.internal.R.array.config_udfps_sensor_props);
+        final boolean isUdfps = !ArrayUtils.isEmpty(udfpsProps);
+        // config_is_powerbutton_fps indicates whether device has a power button fingerprint sensor.
+        final boolean isPowerbuttonFps = context.getResources().getBoolean(
+                R.bool.config_is_powerbutton_fps);
+
+        if (isUdfps) {
+            sensorType = FingerprintSensorType.UNKNOWN;
+        } else if (isPowerbuttonFps) {
+            sensorType = FingerprintSensorType.POWER_BUTTON;
+        } else {
+            sensorType = FingerprintSensorType.REAR;
+        }
+
+        if (isUdfps && udfpsProps.length == 3) {
+            setSensorLocation(udfpsProps[0], udfpsProps[1], udfpsProps[2]);
+        } else {
+            setSensorLocation(540 /* sensorLocationX */, 1636 /* sensorLocationY */,
+                    130 /* sensorRadius */);
+        }
+
+    }
+
+    private void setSensorLocation(int sensorLocationX,
+            int sensorLocationY, int sensorRadius) {
+        sensorLocations[0] = new SensorLocation();
+        sensorLocations[0].display = "";
+        sensorLocations[0].sensorLocationX = sensorLocationX;
+        sensorLocations[0].sensorLocationY = sensorLocationY;
+        sensorLocations[0].sensorRadius = sensorRadius;
+    }
+
+    private byte authenticatorStrengthToPropertyStrength(
+            @BiometricManager.Authenticators.Types int strength) {
+        switch (strength) {
+            case BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE:
+                return SensorStrength.CONVENIENCE;
+            case BiometricManager.Authenticators.BIOMETRIC_WEAK:
+                return SensorStrength.WEAK;
+            case BiometricManager.Authenticators.BIOMETRIC_STRONG:
+                return SensorStrength.STRONG;
+            default:
+                throw new IllegalArgumentException("Unknown strength: " + strength);
+        }
+    }
+}
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index f594c00..606b171 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -31,6 +31,7 @@
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import java.util.List;
 
 /**
@@ -173,6 +174,10 @@
     @EnforcePermission("MANAGE_FINGERPRINT")
     void removeClientActiveCallback(IFingerprintClientActiveCallback callback);
 
+    //Register all available fingerprint sensors.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void registerAuthenticatorsLegacy(in FingerprintSensorConfigurations fingerprintSensorConfigurations);
+
     // Registers all HIDL and AIDL sensors. Only HIDL sensor properties need to be provided, because
     // AIDL sensor properties are retrieved directly from the available HALs. If no HIDL HALs exist,
     // hidlSensors must be non-null and empty. See AuthService.java
diff --git a/core/java/android/os/HidlSupport.java b/core/java/android/os/HidlSupport.java
index 91b796a..7716055 100644
--- a/core/java/android/os/HidlSupport.java
+++ b/core/java/android/os/HidlSupport.java
@@ -218,6 +218,13 @@
     @SystemApi
     public static native int getPidIfSharable();
 
+    /**
+     * Return true if HIDL is supported on this device and false if not.
+     *
+     * @hide
+     */
+    public static native boolean isHidlSupported();
+
     /** @hide */
     public HidlSupport() {}
 }
diff --git a/core/java/android/os/HwBinder.java b/core/java/android/os/HwBinder.java
index feed208..bc19655 100644
--- a/core/java/android/os/HwBinder.java
+++ b/core/java/android/os/HwBinder.java
@@ -18,6 +18,7 @@
 
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.util.Log;
 
 import libcore.util.NativeAllocationRegistry;
 
@@ -78,6 +79,17 @@
             String iface,
             String serviceName)
         throws RemoteException, NoSuchElementException {
+        if (!HidlSupport.isHidlSupported()
+                && (iface.equals("android.hidl.manager@1.0::IServiceManager")
+                        || iface.equals("android.hidl.manager@1.1::IServiceManager")
+                        || iface.equals("android.hidl.manager@1.2::IServiceManager"))) {
+            Log.i(
+                    TAG,
+                    "Replacing Java hwservicemanager with a fake HwNoService"
+                            + " because HIDL is not supported on this device.");
+            return new HwNoService();
+        }
+
         return getService(iface, serviceName, false /* retry */);
     }
     /**
diff --git a/core/java/android/os/HwNoService.java b/core/java/android/os/HwNoService.java
new file mode 100644
index 0000000..117c3ad
--- /dev/null
+++ b/core/java/android/os/HwNoService.java
@@ -0,0 +1,52 @@
+/*
+ * 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.os;
+
+/**
+ * A fake hwservicemanager that is used locally when HIDL isn't supported on the device.
+ *
+ * @hide
+ */
+final class HwNoService implements IHwBinder, IHwInterface {
+    /** @hide */
+    @Override
+    public void transact(int code, HwParcel request, HwParcel reply, int flags) {}
+
+    /** @hide */
+    @Override
+    public IHwInterface queryLocalInterface(String descriptor) {
+        return new HwNoService();
+    }
+
+    /** @hide */
+    @Override
+    public boolean linkToDeath(DeathRecipient recipient, long cookie) {
+        return true;
+    }
+
+    /** @hide */
+    @Override
+    public boolean unlinkToDeath(DeathRecipient recipient) {
+        return true;
+    }
+
+    /** @hide */
+    @Override
+    public IHwBinder asBinder() {
+        return this;
+    }
+}
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 209a595..d3f2c7a 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -92,3 +92,6 @@
 per-file IThermal* = file:/THERMAL_OWNERS
 per-file CoolingDevice.java = file:/THERMAL_OWNERS
 per-file Temperature.java = file:/THERMAL_OWNERS
+
+# SecurityStateManager
+per-file *SecurityStateManager* = file:/SECURITY_STATE_OWNERS
\ No newline at end of file
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 7e07e1f..80ec458 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -757,7 +757,6 @@
                                                   @Nullable String invokeWith,
                                                   @Nullable String packageName,
                                                   @Nullable long[] disabledCompatChanges,
-                                                  boolean bindMountSyspropOverrides,
                                                   @Nullable String[] zygoteArgs) {
         // Webview zygote can't access app private data files, so doesn't need to know its data
         // info.
@@ -767,7 +766,7 @@
                     /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, /*isTopApp=*/ false,
                 disabledCompatChanges, /* pkgDataInfoMap */ null,
                 /* whitelistedDataInfoMap */ null, /* bindMountAppsData */ false,
-                /* bindMountAppStorageDirs */ false, bindMountSyspropOverrides, zygoteArgs);
+                /* bindMountAppStorageDirs */ false, /* bindMountSyspropOverrides */ false, zygoteArgs);
     }
 
     /**
@@ -1599,7 +1598,7 @@
      * fully removed, otherwise system resources may leak.
      * @hide
      */
-    public static final native int sendSignalToProcessGroup(int uid, int pid, int signal);
+    public static final native boolean sendSignalToProcessGroup(int uid, int pid, int signal);
 
     /**
       * Freeze the cgroup for the given UID.
diff --git a/core/java/android/os/ServiceSpecificException.java b/core/java/android/os/ServiceSpecificException.java
index 49ce40b..df503e8 100644
--- a/core/java/android/os/ServiceSpecificException.java
+++ b/core/java/android/os/ServiceSpecificException.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 /**
  * An exception specific to a service.
@@ -33,6 +34,7 @@
  * @hide
  */
 @SystemApi
+@RavenwoodKeepWholeClass
 public class ServiceSpecificException extends RuntimeException {
     public final int errorCode;
 
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 4bfff16..7d00b80 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -6911,7 +6911,11 @@
          * <td></td>
          * </tr>
          * </table>
+         *
+         * @deprecated This field may not be well supported by some contacts apps and is discouraged
+         * to use.
          */
+        @Deprecated
         public static final class Im implements DataColumnsWithJoins, CommonColumns, ContactCounts {
             /**
              * This utility class cannot be instantiated
@@ -7721,7 +7725,11 @@
          * <td></td>
          * </tr>
          * </table>
+         *
+         * @deprecated This field may not be well supported by some contacts apps and is discouraged
+         * to use.
          */
+        @Deprecated
         public static final class SipAddress implements DataColumnsWithJoins, CommonColumns,
                 ContactCounts {
             /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 290ae4a..942ce971 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7330,7 +7330,7 @@
                 "hearing_aid_media_routing";
 
         /**
-         * System sounds routing value for hearing aid. It routes system sounds to hearing aid
+         * Notification routing value for hearing aid. It routes notification sounds to hearing aid
          * or device speaker.
          * <ul>
          *     <li> 0 = Default
@@ -7339,8 +7339,8 @@
          * </ul>
          * @hide
          */
-        public static final String HEARING_AID_SYSTEM_SOUNDS_ROUTING =
-                "hearing_aid_system_sounds_routing";
+        public static final String HEARING_AID_NOTIFICATION_ROUTING =
+                "hearing_aid_notification_routing";
 
         /**
          * Setting to indicate that on device captions are enabled.
@@ -14339,6 +14339,14 @@
         public static final String MODE_RINGER = "mode_ringer";
 
         /**
+         * Whether or not Alarm stream should always be muted with Ringer.
+         *
+         * @hide
+         */
+        public static final String MUTE_ALARM_STREAM_WITH_RINGER_MODE =
+                "mute_alarm_stream_with_ringer_mode";
+
+        /**
          * Overlay display devices setting.
          * The associated value is a specially formatted string that describes the
          * size and density of simulated secondary display devices.
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index 72c436e..e4af2da 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4940,6 +4940,13 @@
          */
         public static final String COLUMN_IS_NTN = "is_ntn";
 
+        /**
+         * TelephonyProvider column name to indicate the service capability bitmasks.
+         *
+         * @hide
+         */
+        public static final String COLUMN_SERVICE_CAPABILITIES = "service_capabilities";
+
         /** All columns in {@link SimInfo} table. */
         private static final List<String> ALL_COLUMNS = List.of(
                 COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID,
@@ -5011,7 +5018,8 @@
                 COLUMN_USER_HANDLE,
                 COLUMN_SATELLITE_ENABLED,
                 COLUMN_SATELLITE_ATTACH_ENABLED_FOR_CARRIER,
-                COLUMN_IS_NTN
+                COLUMN_IS_NTN,
+                COLUMN_SERVICE_CAPABILITIES
         );
 
         /**
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 875031f..23c8393 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -960,8 +960,8 @@
                 mKeyphraseMetadata = new KeyphraseMetadata(1, mText, fakeSupportedLocales,
                         AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER);
             }
+            notifyStateChangedLocked();
         }
-        notifyStateChanged(availability);
     }
 
     /**
@@ -1371,8 +1371,8 @@
 
             mAvailability = STATE_INVALID;
             mIsAvailabilityOverriddenByTestApi = false;
+            notifyStateChangedLocked();
         }
-        notifyStateChanged(STATE_INVALID);
         super.destroy();
     }
 
@@ -1402,8 +1402,6 @@
      */
     // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector
     void onSoundModelsChanged() {
-        boolean notifyError = false;
-
         synchronized (mLock) {
             if (mAvailability == STATE_INVALID
                     || mAvailability == STATE_HARDWARE_UNAVAILABLE
@@ -1444,9 +1442,6 @@
                     // calling stopRecognition where there is no started session.
                     Log.w(TAG, "Failed to stop recognition after enrollment update: code="
                             + result);
-
-                    // Execute a refresh availability task - which should then notify of a change.
-                    new RefreshAvailabilityTask().execute();
                 } catch (Exception e) {
                     Slog.w(TAG, "Failed to stop recognition after enrollment update", e);
                     if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
@@ -1455,14 +1450,14 @@
                                         + Log.getStackTraceString(e),
                                 FailureSuggestedAction.RECREATE_DETECTOR));
                     } else {
-                        notifyError = true;
+                        updateAndNotifyStateChangedLocked(STATE_ERROR);
                     }
+                    return;
                 }
             }
-        }
 
-        if (notifyError) {
-            updateAndNotifyStateChanged(STATE_ERROR);
+            // Execute a refresh availability task - which should then notify of a change.
+            new RefreshAvailabilityTask().execute();
         }
     }
 
@@ -1578,11 +1573,10 @@
         }
     }
 
-    private void updateAndNotifyStateChanged(int availability) {
-        synchronized (mLock) {
-            updateAvailabilityLocked(availability);
-        }
-        notifyStateChanged(availability);
+    @GuardedBy("mLock")
+    private void updateAndNotifyStateChangedLocked(int availability) {
+        updateAvailabilityLocked(availability);
+        notifyStateChangedLocked();
     }
 
     @GuardedBy("mLock")
@@ -1596,17 +1590,17 @@
         }
     }
 
-    private void notifyStateChanged(int newAvailability) {
+    @GuardedBy("mLock")
+    private void notifyStateChangedLocked() {
         Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
-        message.arg1 = newAvailability;
+        message.arg1 = mAvailability;
         message.sendToTarget();
     }
 
+    @GuardedBy("mLock")
     private void sendUnknownFailure(String failureMessage) {
-        synchronized (mLock) {
-            // update but do not call onAvailabilityChanged callback for STATE_ERROR
-            updateAvailabilityLocked(STATE_ERROR);
-        }
+        // update but do not call onAvailabilityChanged callback for STATE_ERROR
+        updateAvailabilityLocked(STATE_ERROR);
         Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget();
     }
 
@@ -1822,17 +1816,19 @@
                             availability = STATE_KEYPHRASE_UNENROLLED;
                         }
                     }
+                    updateAndNotifyStateChangedLocked(availability);
                 }
-                updateAndNotifyStateChanged(availability);
             } catch (Exception e) {
                 // Any exception here not caught will crash the process because AsyncTask does not
                 // bubble up the exceptions to the client app, so we must propagate it to the app.
                 Slog.w(TAG, "Failed to refresh availability", e);
-                if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
-                    sendUnknownFailure(
-                            "Failed to refresh availability: " + Log.getStackTraceString(e));
-                } else {
-                    updateAndNotifyStateChanged(STATE_ERROR);
+                synchronized (mLock) {
+                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
+                        sendUnknownFailure(
+                                "Failed to refresh availability: " + Log.getStackTraceString(e));
+                    } else {
+                        updateAndNotifyStateChangedLocked(STATE_ERROR);
+                    }
                 }
             }
 
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index b7d9705..adc54f5 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -94,7 +94,9 @@
      */
     public void updateState(@Nullable PersistableBundle options,
             @Nullable SharedMemory sharedMemory) {
-        mInitializationDelegate.updateState(options, sharedMemory);
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.updateState(options, sharedMemory);
+        }
     }
 
 
@@ -116,18 +118,21 @@
         if (DEBUG) {
             Slog.i(TAG, "#startRecognition");
         }
-        // check if the detector is active with the initialization delegate
-        mInitializationDelegate.startRecognition();
+        synchronized (mInitializationDelegate.getLock()) {
+            // check if the detector is active with the initialization delegate
+            mInitializationDelegate.startRecognition();
 
-        try {
-            mManagerService.startPerceiving(new BinderCallback(mExecutor, mCallback));
-        } catch (SecurityException e) {
-            Slog.e(TAG, "startRecognition failed: " + e);
-            return false;
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+            try {
+                mManagerService.startPerceiving(new BinderCallback(
+                        mExecutor, mCallback, mInitializationDelegate.getLock()));
+            } catch (SecurityException e) {
+                Slog.e(TAG, "startRecognition failed: " + e);
+                return false;
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            return true;
         }
-        return true;
     }
 
     /**
@@ -140,15 +145,17 @@
         if (DEBUG) {
             Slog.i(TAG, "#stopRecognition");
         }
-        // check if the detector is active with the initialization delegate
-        mInitializationDelegate.startRecognition();
+        synchronized (mInitializationDelegate.getLock()) {
+            // check if the detector is active with the initialization delegate
+            mInitializationDelegate.stopRecognition();
 
-        try {
-            mManagerService.stopPerceiving();
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
+            try {
+                mManagerService.stopPerceiving();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            return true;
         }
-        return true;
     }
 
     /**
@@ -160,12 +167,16 @@
         if (DEBUG) {
             Slog.i(TAG, "#destroy");
         }
-        mInitializationDelegate.destroy();
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.destroy();
+        }
     }
 
     /** @hide */
     public void dump(String prefix, PrintWriter pw) {
-        // TODO: implement this
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.dump(prefix, pw);
+        }
     }
 
     /** @hide */
@@ -175,7 +186,9 @@
 
     /** @hide */
     void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) {
-        mInitializationDelegate.registerOnDestroyListener(onDestroyListener);
+        synchronized (mInitializationDelegate.getLock()) {
+            mInitializationDelegate.registerOnDestroyListener(onDestroyListener);
+        }
     }
 
     /**
@@ -282,6 +295,15 @@
         public boolean isUsingSandboxedDetectionService() {
             return true;
         }
+
+        @Override
+        public void dump(String prefix, PrintWriter pw) {
+            // No-op
+        }
+
+        private Object getLock() {
+            return mLock;
+        }
     }
 
     private static class BinderCallback
@@ -289,31 +311,43 @@
         private final Executor mExecutor;
         private final VisualQueryDetector.Callback mCallback;
 
-        BinderCallback(Executor executor, VisualQueryDetector.Callback callback) {
+        private final Object mLock;
+
+        BinderCallback(Executor executor, VisualQueryDetector.Callback callback, Object lock) {
             this.mExecutor = executor;
             this.mCallback = callback;
+            this.mLock = lock;
         }
 
         /** Called when the detected result is valid. */
         @Override
         public void onQueryDetected(@NonNull String partialQuery) {
             Slog.v(TAG, "BinderCallback#onQueryDetected");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onQueryDetected(partialQuery)));
+            Binder.withCleanCallingIdentity(() -> {
+                synchronized (mLock) {
+                    mCallback.onQueryDetected(partialQuery);
+                }
+            });
         }
 
         @Override
         public void onQueryFinished() {
             Slog.v(TAG, "BinderCallback#onQueryFinished");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onQueryFinished()));
+            Binder.withCleanCallingIdentity(() -> {
+                synchronized (mLock) {
+                    mCallback.onQueryFinished();
+                }
+            });
         }
 
         @Override
         public void onQueryRejected() {
             Slog.v(TAG, "BinderCallback#onQueryRejected");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onQueryRejected()));
+            Binder.withCleanCallingIdentity(() -> {
+                synchronized (mLock) {
+                    mCallback.onQueryRejected();
+                }
+            });
         }
 
         /** Called when the detection fails due to an error. */
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index 31d759e..18080e4 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -287,6 +287,16 @@
     }
 
     /**
+     * Called when a display hdcp levels change event is received.
+     *
+     * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+     * @param connectedLevel the new connected HDCP level
+     * @param maxLevel the maximum HDCP level
+     */
+    public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) {
+    }
+
+    /**
      * Represents a mapping between a UID and an override frame rate
      */
     public static class FrameRateOverride {
@@ -374,4 +384,11 @@
         onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
     }
 
+    // Called from native code.
+    @SuppressWarnings("unused")
+    private void dispatchHdcpLevelsChanged(long physicalDisplayId, int connectedLevel,
+            int maxLevel) {
+        onHdcpLevelsChanged(physicalDisplayId, connectedLevel, maxLevel);
+    }
+
 }
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index cabab6c..0927d45 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -17,7 +17,6 @@
 package android.view;
 
 import static android.view.InsetsSourceProto.FRAME;
-import static android.view.InsetsSourceProto.TYPE;
 import static android.view.InsetsSourceProto.TYPE_NUMBER;
 import static android.view.InsetsSourceProto.VISIBLE;
 import static android.view.InsetsSourceProto.VISIBLE_FRAME;
@@ -353,13 +352,12 @@
      */
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
-        proto.write(TYPE, WindowInsets.Type.toString(mType));
-        proto.write(TYPE_NUMBER, mType);
         mFrame.dumpDebug(proto, FRAME);
         if (mVisibleFrame != null) {
             mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME);
         }
         proto.write(VISIBLE, mVisible);
+        proto.write(TYPE_NUMBER, mType);
         proto.end(token);
     }
 
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index 34b2884..0ce61bb 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -21,11 +21,11 @@
 import static android.view.InsetsController.DEBUG;
 import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE;
 import static android.view.InsetsSourceConsumerProto.HAS_WINDOW_FOCUS;
-import static android.view.InsetsSourceConsumerProto.INTERNAL_INSETS_TYPE;
 import static android.view.InsetsSourceConsumerProto.IS_REQUESTED_VISIBLE;
 import static android.view.InsetsSourceConsumerProto.PENDING_FRAME;
 import static android.view.InsetsSourceConsumerProto.PENDING_VISIBLE_FRAME;
 import static android.view.InsetsSourceConsumerProto.SOURCE_CONTROL;
+import static android.view.InsetsSourceConsumerProto.TYPE_NUMBER;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 
@@ -393,7 +393,6 @@
 
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
-        proto.write(INTERNAL_INSETS_TYPE, WindowInsets.Type.toString(mType));
         proto.write(HAS_WINDOW_FOCUS, mHasWindowFocus);
         proto.write(IS_REQUESTED_VISIBLE, isShowRequested());
         if (mSourceControl != null) {
@@ -406,6 +405,7 @@
             mPendingVisibleFrame.dumpDebug(proto, PENDING_VISIBLE_FRAME);
         }
         proto.write(ANIMATION_STATE, mAnimationState);
+        proto.write(TYPE_NUMBER, mType);
         proto.end(token);
     }
 }
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 7ea93f5..527c7ed 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -20,7 +20,7 @@
 import static android.graphics.PointProto.Y;
 import static android.view.InsetsSourceControlProto.LEASH;
 import static android.view.InsetsSourceControlProto.POSITION;
-import static android.view.InsetsSourceControlProto.TYPE;
+import static android.view.InsetsSourceControlProto.TYPE_NUMBER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -244,8 +244,6 @@
      */
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
-        proto.write(TYPE, WindowInsets.Type.toString(mType));
-
         final long surfaceToken = proto.start(POSITION);
         proto.write(X, mSurfacePosition.x);
         proto.write(Y, mSurfacePosition.y);
@@ -254,6 +252,8 @@
         if (mLeash != null) {
             mLeash.dumpDebug(proto, LEASH);
         }
+
+        proto.write(TYPE_NUMBER, mType);
         proto.end(token);
     }
 
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index cbbe785..b957b31 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -22,7 +22,6 @@
 import static android.graphics.Matrix.MSKEW_Y;
 import static android.graphics.Matrix.MTRANS_X;
 import static android.graphics.Matrix.MTRANS_Y;
-import static android.view.Display.INVALID_DISPLAY;
 import static android.view.SurfaceControlProto.HASH_CODE;
 import static android.view.SurfaceControlProto.LAYER_ID;
 import static android.view.SurfaceControlProto.NAME;
@@ -38,7 +37,6 @@
 import android.annotation.Size;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.content.Context;
 import android.graphics.ColorSpace;
 import android.graphics.GraphicBuffer;
 import android.graphics.Matrix;
@@ -53,13 +51,8 @@
 import android.hardware.OverlayProperties;
 import android.hardware.SyncFence;
 import android.hardware.display.DeviceProductInfo;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
-import android.hardware.display.IDisplayManager;
-import android.hardware.display.IVirtualDisplayCallback;
-import android.hardware.display.VirtualDisplay;
 import android.hardware.graphics.common.DisplayDecorationSupport;
 import android.opengl.EGLDisplay;
 import android.opengl.EGLSync;
@@ -68,8 +61,6 @@
 import android.os.Looper;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
@@ -2355,92 +2346,6 @@
     }
 
     /**
-     * Because this API is now going through {@link DisplayManager}, orientation and displayRect
-     * will automatically be computed based on configuration changes. Because of this, the params
-     * orientation and displayRect are ignored
-     *
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
-            publicAlternatives = "Use {@code VirtualDisplay#resize(int, int, int)} instead.",
-            trackingBug = 247078497)
-    public static void setDisplayProjection(IBinder displayToken, int orientation,
-            Rect layerStackRect, Rect displayRect) {
-        DisplayManagerGlobal.getInstance().resizeVirtualDisplay(
-                IVirtualDisplayCallback.Stub.asInterface(displayToken), layerStackRect.width(),
-                layerStackRect.height(), 1);
-    }
-
-    /**
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
-            publicAlternatives = "Use {@code MediaProjection#createVirtualDisplay()} with flag "
-                    + " {@code VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR} for mirroring instead.",
-            trackingBug = 247078497)
-    public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
-        IBinder b = ServiceManager.getService(Context.DISPLAY_SERVICE);
-        if (b == null) {
-            throw new UnsupportedOperationException();
-        }
-
-        IDisplayManager dm = IDisplayManager.Stub.asInterface(b);
-        try {
-            dm.setDisplayIdToMirror(displayToken, layerStack);
-        } catch (RemoteException e) {
-            throw new UnsupportedOperationException(e);
-        }
-    }
-
-    /**
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
-            publicAlternatives = "Use {@code VirtualDisplay#setSurface(Surface)} instead.",
-            trackingBug = 247078497)
-    public static void setDisplaySurface(IBinder displayToken, Surface surface) {
-        IVirtualDisplayCallback virtualDisplayCallback =
-                IVirtualDisplayCallback.Stub.asInterface(displayToken);
-        DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
-        dm.setVirtualDisplaySurface(virtualDisplayCallback, surface);
-    }
-
-    /**
-     * Secure is no longer supported because this is only called from outside system which cannot
-     * create secure displays.
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
-            publicAlternatives = "Use {@code MediaProjection#createVirtualDisplay()} or "
-                    + "{@code DisplayManager#createVirtualDisplay()} instead.",
-            trackingBug = 247078497)
-    public static IBinder createDisplay(String name, boolean secure) {
-        if (name == null) {
-            throw new IllegalArgumentException("name must not be null");
-        }
-
-        // We don't have a size yet so pass in 1 for width and height since 0 is invalid
-        VirtualDisplay vd = DisplayManager.createVirtualDisplay(name, 1 /* width */, 1 /* height */,
-                INVALID_DISPLAY, null /* Surface */);
-        return vd == null ? null : vd.getToken().asBinder();
-    }
-
-    /**
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
-            publicAlternatives = "Use {@code VirtualDisplay#release()} instead.",
-            trackingBug = 247078497)
-    public static void destroyDisplay(IBinder displayToken) {
-        if (displayToken == null) {
-            throw new IllegalArgumentException("displayToken must not be null");
-        }
-
-        DisplayManagerGlobal.getInstance().releaseVirtualDisplay(
-                IVirtualDisplayCallback.Stub.asInterface(displayToken));
-    }
-
-    /**
      * Returns whether protected content is supported in GPU composition.
      * @hide
      */
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index cbafd1c..ec99459 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -32,6 +32,7 @@
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision;
 import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
 import static android.view.flags.Flags.viewVelocityApi;
 import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;
@@ -2309,6 +2310,7 @@
     protected static final int[] PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
 
     private static boolean sToolkitSetFrameRateReadOnlyFlagValue;
+    private static boolean sToolkitMetricsForFrameRateDecisionFlagValue;
 
     static {
         EMPTY_STATE_SET = StateSet.get(0);
@@ -2393,6 +2395,7 @@
                         | StateSet.VIEW_STATE_PRESSED);
 
         sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
+        sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision();
     }
 
     /**
@@ -33056,7 +33059,10 @@
     }
 
     private float getSizePercentage() {
-        if (mResources == null || getVisibility() != VISIBLE) {
+        float alpha = mTransformationInfo != null ? mTransformationInfo.mAlpha : 1;
+        int visibility = mViewFlags & VISIBILITY_MASK;
+
+        if (mResources == null || alpha == 0 || visibility != VISIBLE) {
             return 0;
         }
 
@@ -33084,22 +33090,26 @@
         ViewRootImpl viewRootImpl = getViewRootImpl();
         float sizePercentage = getSizePercentage();
         int frameRateCateogry = calculateFrameRateCategory(sizePercentage);
-        if (sToolkitSetFrameRateReadOnlyFlagValue && viewRootImpl != null
-                && sizePercentage > 0) {
-            if (mPreferredFrameRate < 0) {
-                if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
-                    frameRateCateogry = FRAME_RATE_CATEGORY_NO_PREFERENCE;
-                } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) {
-                    frameRateCateogry = FRAME_RATE_CATEGORY_LOW;
-                } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) {
-                    frameRateCateogry = FRAME_RATE_CATEGORY_NORMAL;
-                } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) {
-                    frameRateCateogry = FRAME_RATE_CATEGORY_HIGH;
+        if (viewRootImpl != null && sizePercentage > 0) {
+            if (sToolkitSetFrameRateReadOnlyFlagValue) {
+                if (mPreferredFrameRate < 0) {
+                    if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
+                        frameRateCateogry = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+                    } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) {
+                        frameRateCateogry = FRAME_RATE_CATEGORY_LOW;
+                    } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) {
+                        frameRateCateogry = FRAME_RATE_CATEGORY_NORMAL;
+                    } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) {
+                        frameRateCateogry = FRAME_RATE_CATEGORY_HIGH;
+                    }
+                } else {
+                    viewRootImpl.votePreferredFrameRate(mPreferredFrameRate);
                 }
-            } else {
-                viewRootImpl.votePreferredFrameRate(mPreferredFrameRate);
+                viewRootImpl.votePreferredFrameRateCategory(frameRateCateogry);
             }
-            viewRootImpl.votePreferredFrameRateCategory(frameRateCateogry);
+            if (sToolkitMetricsForFrameRateDecisionFlagValue) {
+                viewRootImpl.recordViewPercentage(sizePercentage);
+            }
         }
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e83488e..1f81a64 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -235,6 +235,7 @@
 import com.android.internal.view.BaseSurfaceHolder;
 import com.android.internal.view.RootViewSurfaceTaker;
 import com.android.internal.view.SurfaceCallbackHelper;
+import com.android.modules.expresslog.Counter;
 import com.android.window.flags.Flags;
 
 import java.io.IOException;
@@ -252,7 +253,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.function.Predicate;
-
 /**
  * The top of a view hierarchy, implementing the needed protocol between View
  * and the WindowManager.  This is for the most part an internal implementation
@@ -528,8 +528,6 @@
     // Set to true to stop input during an Activity Transition.
     boolean mPausedForTransition = false;
 
-    boolean mLastInCompatMode = false;
-
     SurfaceHolder.Callback2 mSurfaceHolderCallback;
     BaseSurfaceHolder mSurfaceHolder;
     boolean mIsCreating;
@@ -830,6 +828,8 @@
     private boolean mInsetsAnimationRunning;
 
     private long mPreviousFrameDrawnTime = -1;
+    // The largest view size percentage to the display size. Used on trace to collect metric.
+    private float mLargestChildPercentage = 0.0f;
 
     /**
      * The resolved pointer icon type requested by this window.
@@ -1069,6 +1069,7 @@
 
     private String mTag = TAG;
     private String mFpsTraceName;
+    private String mLargestViewTraceName;
 
     private static boolean sToolkitSetFrameRateReadOnlyFlagValue;
     private static boolean sToolkitMetricsForFrameRateDecisionFlagValue;
@@ -1320,6 +1321,7 @@
                 attrs = mWindowAttributes;
                 setTag();
                 mFpsTraceName = "FPS of " + getTitle();
+                mLargestViewTraceName = "Largest view percentage(per hundred) of " + getTitle();
 
                 if (DEBUG_KEEP_SCREEN_ON && (mClientWindowLayoutFlags
                         & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
@@ -1375,11 +1377,6 @@
                 }
                 if (DEBUG_LAYOUT) Log.d(mTag, "WindowLayout in setView:" + attrs);
 
-                if (!compatibilityInfo.supportsScreen()) {
-                    attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-                    mLastInCompatMode = true;
-                }
-
                 mSoftInputMode = attrs.softInputMode;
                 mWindowAttributesChanged = true;
                 mAttachInfo.mRootView = view;
@@ -1913,10 +1910,6 @@
             // Keep track of the actual window flags supplied by the client.
             mClientWindowLayoutFlags = attrs.flags;
 
-            // Preserve compatible window flag if exists.
-            final int compatibleWindowFlag = mWindowAttributes.privateFlags
-                    & WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-
             // Preserve system UI visibility.
             final int systemUiVisibility = mWindowAttributes.systemUiVisibility;
             final int subtreeSystemUiVisibility = mWindowAttributes.subtreeSystemUiVisibility;
@@ -1948,8 +1941,7 @@
             mWindowAttributes.subtreeSystemUiVisibility = subtreeSystemUiVisibility;
             mWindowAttributes.insetsFlags.appearance = appearance;
             mWindowAttributes.insetsFlags.behavior = behavior;
-            mWindowAttributes.privateFlags |= compatibleWindowFlag
-                    | appearanceAndBehaviorPrivateFlags;
+            mWindowAttributes.privateFlags |= appearanceAndBehaviorPrivateFlags;
 
             if (mWindowAttributes.preservePreviousSurfaceInsets) {
                 // Restore old surface insets.
@@ -3150,21 +3142,6 @@
         final boolean shouldOptimizeMeasure = shouldOptimizeMeasure(lp);
 
         WindowManager.LayoutParams params = null;
-        CompatibilityInfo compatibilityInfo =
-                mDisplay.getDisplayAdjustments().getCompatibilityInfo();
-        if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
-            params = lp;
-            mFullRedrawNeeded = true;
-            mLayoutRequested = true;
-            if (mLastInCompatMode) {
-                params.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-                mLastInCompatMode = false;
-            } else {
-                params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-                mLastInCompatMode = true;
-            }
-        }
-
         Rect frame = mWinFrame;
         if (mFirst) {
             mFullRedrawNeeded = true;
@@ -4766,6 +4743,10 @@
         long fps = NANOS_PER_SEC / timeDiff;
         Trace.setCounter(mFpsTraceName, fps);
         mPreviousFrameDrawnTime = expectedDrawnTime;
+
+        long percentage = (long) (mLargestChildPercentage * 100);
+        Trace.setCounter(mLargestViewTraceName, percentage);
+        mLargestChildPercentage = 0.0f;
     }
 
     private void reportDrawFinished(@Nullable Transaction t, int seqId) {
@@ -5086,6 +5067,7 @@
         if (DEBUG_FPS) {
             trackFPS();
         }
+
         if (sToolkitMetricsForFrameRateDecisionFlagValue) {
             collectFrameRateDecisionMetrics();
         }
@@ -9723,6 +9705,9 @@
             } else {
                 q.mReceiver.finishInputEvent(q.mEvent, handled);
             }
+            if (q.mEvent instanceof KeyEvent) {
+                logHandledSystemKey((KeyEvent) q.mEvent, handled);
+            }
         } else {
             q.mEvent.recycleIfNeededAfterDispatch();
         }
@@ -9730,6 +9715,19 @@
         recycleQueuedInputEvent(q);
     }
 
+    private void logHandledSystemKey(KeyEvent event, boolean handled) {
+        final int keyCode = event.getKeyCode();
+        if (keyCode != KeyEvent.KEYCODE_STEM_PRIMARY) {
+            return;
+        }
+        if (event.isDown() && event.getRepeatCount() == 0 && handled) {
+            // Initial DOWN event is handled. Log the stem primary key press.
+            Counter.logIncrementWithUid(
+                    "input.value_app_handled_stem_primary_key_gestures_count",
+                    Process.myUid());
+        }
+    }
+
     static boolean isTerminalInputEvent(InputEvent event) {
         if (event instanceof KeyEvent) {
             final KeyEvent keyEvent = (KeyEvent)event;
@@ -12175,7 +12173,8 @@
                 || motionEventAction == MotionEvent.ACTION_UP;
         boolean undesiredType = windowType == TYPE_INPUT_METHOD;
         // use toolkitSetFrameRate flag to gate the change
-        return desiredAction && !undesiredType && sToolkitSetFrameRateReadOnlyFlagValue;
+        return desiredAction && !undesiredType && sToolkitSetFrameRateReadOnlyFlagValue
+                && getFrameRateBoostOnTouchEnabled();
     }
 
     /**
@@ -12250,6 +12249,15 @@
         return mIsFrameRateBoosting;
     }
 
+    /**
+     * Get the value of mFrameRateBoostOnTouchEnabled
+     * Can be used to checked if touch boost is enabled. The default value is true.
+     */
+    @VisibleForTesting
+    public boolean getFrameRateBoostOnTouchEnabled() {
+        return mWindowAttributes.getFrameRateBoostOnTouchEnabled();
+    }
+
     private void boostFrameRate(int boostTimeOut) {
         mIsFrameRateBoosting = true;
         setPreferredFrameRateCategory(mPreferredFrameRateCategory);
@@ -12279,4 +12287,10 @@
     void setBackKeyCallbackForWindowlessWindow(@NonNull Predicate<KeyEvent> callback) {
         mWindowlessBackKeyCallback = callback;
     }
+
+    void recordViewPercentage(float percentage) {
+        if (!Trace.isEnabled()) return;
+        // Record the largest view of percentage to the display size.
+        mLargestChildPercentage = Math.max(percentage, mLargestChildPercentage);
+    }
 }
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 87537fbc..7bae7ec 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -334,6 +334,9 @@
     private boolean mOverlayWithDecorCaptionEnabled = true;
     private boolean mCloseOnSwipeEnabled = false;
 
+    private static boolean sToolkitSetFrameRateReadOnlyFlagValue =
+                android.view.flags.Flags.toolkitSetFrameRateReadOnly();
+
     // The current window attributes.
     @UnsupportedAppUsage
     private final WindowManager.LayoutParams mWindowAttributes =
@@ -1373,6 +1376,39 @@
     }
 
     /**
+     * Sets whether the frame rate touch boost is enabled for this Window.
+     * When enabled, the frame rate will be boosted when a user touches the Window.
+     *
+     * @param enabled whether the frame rate touch boost is enabled.
+     * @see #getFrameRateBoostOnTouchEnabled()
+     * @see WindowManager.LayoutParams#setFrameRateBoostOnTouchEnabled(boolean)
+     */
+    @FlaggedApi(android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+    public void setFrameRateBoostOnTouchEnabled(boolean enabled) {
+        if (sToolkitSetFrameRateReadOnlyFlagValue) {
+            final WindowManager.LayoutParams attrs = getAttributes();
+            attrs.setFrameRateBoostOnTouchEnabled(enabled);
+            dispatchWindowAttributesChanged(attrs);
+        }
+    }
+
+    /**
+     * Get whether frame rate touch boost is enabled
+     * {@link #setFrameRateBoostOnTouchEnabled(boolean)}
+     *
+     * @return whether the frame rate touch boost is enabled.
+     * @see #setFrameRateBoostOnTouchEnabled(boolean)
+     * @see WindowManager.LayoutParams#getFrameRateBoostOnTouchEnabled()
+     */
+    @FlaggedApi(android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+    public boolean getFrameRateBoostOnTouchEnabled() {
+        if (sToolkitSetFrameRateReadOnlyFlagValue) {
+            return getAttributes().getFrameRateBoostOnTouchEnabled();
+        }
+        return true;
+    }
+
+    /**
      * If {@code isPreferred} is true, this method requests that the connected display does minimal
      * post processing when this window is visible on the screen. Otherwise, it requests that the
      * display switches back to standard image processing.
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 57a4161..921afaa 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -38,6 +38,8 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
 import android.graphics.Insets;
@@ -1519,6 +1521,9 @@
         }
 
         /** @hide */
+        @TestApi
+        @NonNull
+        @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
         public static String toString(@InsetsType int types) {
             StringBuilder result = new StringBuilder();
             if ((types & STATUS_BARS) != 0) {
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index e3d9c60..f76822f 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -122,7 +122,6 @@
 import android.view.WindowInsets.Type;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.accessibility.AccessibilityNodeInfo;
-import android.window.ITrustedPresentationListener;
 import android.window.TaskFpsCallback;
 import android.window.TrustedPresentationThresholds;
 
@@ -1280,9 +1279,8 @@
      *     android:value="true|false"/&gt;
      * &lt;/application&gt;
      * </pre>
-     * @hide
      */
-    // TODO(b/279428317): Make this public API.
+    @FlaggedApi(Flags.FLAG_APP_COMPAT_PROPERTIES_API)
     String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE =
             "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
 
@@ -3145,13 +3143,6 @@
         @UnsupportedAppUsage
         public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 1 << 6;
 
-        /** Window flag: special flag to limit the size of the window to be
-         * original size ([320x480] x density). Used to create window for applications
-         * running under compatibility mode.
-         *
-         * {@hide} */
-        public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 1 << 7;
-
         /** Window flag: a special option intended for system dialogs.  When
          * this flag is set, the window will demand focus unconditionally when
          * it is created.
@@ -3347,7 +3338,6 @@
                 SYSTEM_FLAG_SHOW_FOR_ALL_USERS,
                 PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION,
                 PRIVATE_FLAG_NO_MOVE_ANIMATION,
-                PRIVATE_FLAG_COMPATIBLE_WINDOW,
                 PRIVATE_FLAG_SYSTEM_ERROR,
                 PRIVATE_FLAG_OPTIMIZE_MEASURE,
                 PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
@@ -3401,10 +3391,6 @@
                         equals = PRIVATE_FLAG_NO_MOVE_ANIMATION,
                         name = "NO_MOVE_ANIMATION"),
                 @ViewDebug.FlagToString(
-                        mask = PRIVATE_FLAG_COMPATIBLE_WINDOW,
-                        equals = PRIVATE_FLAG_COMPATIBLE_WINDOW,
-                        name = "COMPATIBLE_WINDOW"),
-                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_SYSTEM_ERROR,
                         equals = PRIVATE_FLAG_SYSTEM_ERROR,
                         name = "SYSTEM_ERROR"),
@@ -4346,6 +4332,13 @@
         private float mDesiredHdrHeadroom = 0;
 
         /**
+         * For variable refresh rate project.
+         */
+        private boolean mFrameRateBoostOnTouch = true;
+        private static boolean sToolkitSetFrameRateReadOnlyFlagValue =
+                android.view.flags.Flags.toolkitSetFrameRateReadOnly();
+
+        /**
          * Carries the requests about {@link WindowInsetsController.Appearance} and
          * {@link WindowInsetsController.Behavior} to the system windows which can produce insets.
          *
@@ -4780,6 +4773,32 @@
         }
 
         /**
+         * Set the value whether we should enable Touch Boost
+         *
+         * @param enabled Whether we should enable Touch Boost
+         */
+        @FlaggedApi(android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+        public void setFrameRateBoostOnTouchEnabled(boolean enabled) {
+            if (sToolkitSetFrameRateReadOnlyFlagValue) {
+                mFrameRateBoostOnTouch = enabled;
+            }
+        }
+
+        /**
+         * Get the value whether we should enable touch boost as set
+         * by {@link #setFrameRateBoostOnTouchEnabled(boolean)}
+         *
+         * @return A boolean value to indicate whether we should enable touch boost
+         */
+        @FlaggedApi(android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+        public boolean getFrameRateBoostOnTouchEnabled() {
+            if (sToolkitSetFrameRateReadOnlyFlagValue) {
+                return mFrameRateBoostOnTouch;
+            }
+            return true;
+        }
+
+        /**
          * <p>
          * Blurs the screen behind the window. The effect is similar to that of {@link #dimAmount},
          * but instead of dimmed, the content behind the window will be blurred (or combined with
@@ -4930,6 +4949,9 @@
             out.writeTypedArray(paramsForRotation, 0 /* parcelableFlags */);
             out.writeInt(mDisplayFlags);
             out.writeFloat(mDesiredHdrHeadroom);
+            if (sToolkitSetFrameRateReadOnlyFlagValue) {
+                out.writeBoolean(mFrameRateBoostOnTouch);
+            }
         }
 
         public static final @android.annotation.NonNull Parcelable.Creator<LayoutParams> CREATOR
@@ -5002,6 +5024,9 @@
             paramsForRotation = in.createTypedArray(LayoutParams.CREATOR);
             mDisplayFlags = in.readInt();
             mDesiredHdrHeadroom = in.readFloat();
+            if (sToolkitSetFrameRateReadOnlyFlagValue) {
+                mFrameRateBoostOnTouch = in.readBoolean();
+            }
         }
 
         @SuppressWarnings({"PointlessBitwiseExpression"})
@@ -5338,6 +5363,12 @@
                 changes |= LAYOUT_CHANGED;
             }
 
+            if (sToolkitSetFrameRateReadOnlyFlagValue
+                    && mFrameRateBoostOnTouch != o.mFrameRateBoostOnTouch) {
+                mFrameRateBoostOnTouch = o.mFrameRateBoostOnTouch;
+                changes |= LAYOUT_CHANGED;
+            }
+
             return changes;
         }
 
@@ -5560,6 +5591,11 @@
                 sb.append(prefix).append("  forciblyShownTypes=").append(
                         WindowInsets.Type.toString(forciblyShownTypes));
             }
+            if (sToolkitSetFrameRateReadOnlyFlagValue && mFrameRateBoostOnTouch) {
+                sb.append(System.lineSeparator());
+                sb.append(prefix).append("  frameRateBoostOnTouch=");
+                sb.append(mFrameRateBoostOnTouch);
+            }
             if (paramsForRotation != null && paramsForRotation.length != 0) {
                 sb.append(System.lineSeparator());
                 sb.append(prefix).append("  paramsForRotation:");
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 53aed49..49a2843 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -5400,13 +5400,10 @@
         public static final AccessibilityAction ACTION_PREVIOUS_HTML_ELEMENT =
                 new AccessibilityAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
 
+        // TODO(316638728): restore ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT in javadoc
         /**
          * Action to scroll the node content forward.
          *
-         * <p>
-         *     <strong>Arguments:</strong>
-         *     {@link #ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT}. This is an optional argument.
-         * </p>
          * <p>The UI element that implements this should send a
          * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. Depending on the orientation,
          * this element should also add the relevant directional scroll actions of
@@ -5447,12 +5444,10 @@
         public static final AccessibilityAction ACTION_SCROLL_FORWARD =
                 new AccessibilityAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
 
+        // TODO(316638728): restore ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT in javadoc
         /**
          * Action to scroll the node content backward.
-         * <p>
-         *     <strong>Arguments:</strong>
-         *     {@link #ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT}. This is an optional argument.
-         * </p>
+         *
          * <p>The UI element that implements this should send a
          * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. Depending on the orientation,
          * this element should also add the relevant directional scroll actions of
@@ -5647,48 +5642,40 @@
         @NonNull public static final AccessibilityAction ACTION_SCROLL_IN_DIRECTION =
                 new AccessibilityAction(R.id.accessibilityActionScrollInDirection);
 
+        // TODO(316638728): restore ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT in javadoc
         /**
          * Action to scroll the node content up.
-         * <p>
-         *     <strong>Arguments:</strong>
-         *     {@link #ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT}. This is an optional argument.
-         * </p>
+         *
          * <p>The UI element that implements this should send a
          * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
          */
         public static final AccessibilityAction ACTION_SCROLL_UP =
                 new AccessibilityAction(R.id.accessibilityActionScrollUp);
 
+        // TODO(316638728): restore ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT in javadoc
         /**
          * Action to scroll the node content left.
-         * <p>
-         *     <strong>Arguments:</strong>
-         *     {@link #ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT}. This is an optional argument.
-         * </p>
+         *
          * <p>The UI element that implements this should send a
          * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
          */
         public static final AccessibilityAction ACTION_SCROLL_LEFT =
                 new AccessibilityAction(R.id.accessibilityActionScrollLeft);
 
+        // TODO(316638728): restore ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT in javadoc
         /**
          * Action to scroll the node content down.
-         * <p>
-         *     <strong>Arguments:</strong>
-         *     {@link #ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT}. This is an optional argument.
-         * </p>
+         *
          * <p>The UI element that implements this should send a
          * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
          */
         public static final AccessibilityAction ACTION_SCROLL_DOWN =
                 new AccessibilityAction(R.id.accessibilityActionScrollDown);
 
+        // TODO(316638728): restore ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT in javadoc
         /**
          * Action to scroll the node content right.
-         * <p>
-         *     <strong>Arguments:</strong>
-         *     {@link #ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT}. This is an optional argument.
-         * </p>
+         *
          * <p>The UI element that implements this should send a
          * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
          */
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
index b3359b7..70d8abe 100644
--- a/core/java/android/view/animation/AnimationUtils.java
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -23,9 +23,6 @@
 import android.annotation.FlaggedApi;
 import android.annotation.InterpolatorRes;
 import android.annotation.TestApi;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
-import android.compat.annotation.Overridable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Resources;
@@ -55,18 +52,6 @@
     private static final int TOGETHER = 0;
     private static final int SEQUENTIALLY = 1;
 
-     /**
-     * For apps targeting {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and above,
-     * this change ID enables to use expectedPresentationTime instead of the frameTime
-     * for the frame start time .
-     *
-     * @hide
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    @Overridable
-    public static final long OVERRIDE_ENABLE_EXPECTED_PRSENTATION_TIME = 278730197L;
-
     private static boolean sExpectedPresentationTimeFlagValue;
     static {
         sExpectedPresentationTimeFlagValue = expectedPresentationTimeReadOnly();
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 966161f..19ba316 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -45,6 +45,7 @@
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
 import android.os.Trace;
+import android.service.contentcapture.ContentCaptureService;
 import android.text.Selection;
 import android.text.Spannable;
 import android.text.TextUtils;
@@ -69,6 +70,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -152,7 +154,16 @@
     public ComponentName mComponentName;
 
     /**
-     * List of events held to be sent as a batch.
+     * Thread-safe queue of events held to be processed as a batch.
+     *
+     * Because it is not guaranteed that the events will be enqueued from a single thread, the
+     * implementation must be thread-safe to prevent unexpected behaviour.
+     */
+    @NonNull
+    private final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue;
+
+    /**
+     * List of events held to be sent to the {@link ContentCaptureService} as a batch.
      *
      * @hide
      */
@@ -238,6 +249,8 @@
         mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
 
         mSessionStateReceiver = new SessionStateReceiver(this);
+
+        mEventProcessQueue = new ConcurrentLinkedQueue<>();
     }
 
     @Override
@@ -733,6 +746,9 @@
         }
         mDirectServiceInterface = null;
         mContentProtectionEventProcessor = null;
+        if (runOnBackgroundThreadEnabled()) {
+            mEventProcessQueue.clear();
+        }
     }
 
     // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
@@ -823,27 +839,30 @@
     // change should also get get rid of the "internalNotifyXXXX" methods above
     void notifyChildSessionStarted(int parentSessionId, int childSessionId,
             @NonNull ContentCaptureContext clientContext) {
-        runOnContentCaptureThread(
-                () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
-                .setParentSessionId(parentSessionId).setClientContext(clientContext),
-                FORCE_FLUSH));
+        final ContentCaptureEvent event =
+                new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+                        .setParentSessionId(parentSessionId)
+                        .setClientContext(clientContext);
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     void notifyChildSessionFinished(int parentSessionId, int childSessionId) {
-        runOnContentCaptureThread(
-                () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
-                .setParentSessionId(parentSessionId), FORCE_FLUSH));
+        final ContentCaptureEvent event =
+                new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+                        .setParentSessionId(parentSessionId);
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
-        runOnContentCaptureThread(() ->
-                sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
-                .setViewNode(node.mNode)));
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
+                .setViewNode(node.mNode);
+        enqueueEvent(event);
     }
 
     void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
-        runOnContentCaptureThread(() -> sendEvent(
-                new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id)));
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED)
+                .setAutofillId(id);
+        enqueueEvent(event);
     }
 
     void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) {
@@ -867,50 +886,90 @@
 
         final int startIndex = Selection.getSelectionStart(text);
         final int endIndex = Selection.getSelectionEnd(text);
-        runOnContentCaptureThread(() -> sendEvent(
-                new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
-                        .setAutofillId(id).setText(eventText)
-                        .setComposingIndex(composingStart, composingEnd)
-                        .setSelectionIndex(startIndex, endIndex)));
+
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
+                .setAutofillId(id).setText(eventText)
+                .setComposingIndex(composingStart, composingEnd)
+                .setSelectionIndex(startIndex, endIndex);
+        enqueueEvent(event);
     }
 
     void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
-        runOnContentCaptureThread(() ->
-                sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
-                .setInsets(viewInsets)));
+        final ContentCaptureEvent event =
+                new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
+                        .setInsets(viewInsets);
+        enqueueEvent(event);
     }
 
     void notifyViewTreeEvent(int sessionId, boolean started) {
         final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
         final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled();
+        final boolean forceFlush = disableFlush ? !started : FORCE_FLUSH;
 
-        runOnContentCaptureThread(() -> sendEvent(
-                new ContentCaptureEvent(sessionId, type),
-                disableFlush ? !started : FORCE_FLUSH));
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type);
+        enqueueEvent(event, forceFlush);
     }
 
     void notifySessionResumed(int sessionId) {
-        runOnContentCaptureThread(() -> sendEvent(
-                new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH));
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED);
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     void notifySessionPaused(int sessionId) {
-        runOnContentCaptureThread(() -> sendEvent(
-                new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH));
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED);
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
-        runOnContentCaptureThread(() ->
-                sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
-                .setClientContext(context), FORCE_FLUSH));
+        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
+                .setClientContext(context);
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     /** public because is also used by ViewRootImpl */
     public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
-        runOnContentCaptureThread(() -> sendEvent(
+        final ContentCaptureEvent event =
                 new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
-                .setBounds(bounds)
-        ));
+                        .setBounds(bounds);
+        enqueueEvent(event);
+    }
+
+    private List<ContentCaptureEvent> clearBufferEvents() {
+        final ArrayList<ContentCaptureEvent> bufferEvents = new ArrayList<>();
+        ContentCaptureEvent event;
+        while ((event = mEventProcessQueue.poll()) != null) {
+            bufferEvents.add(event);
+        }
+        return bufferEvents;
+    }
+
+    private void enqueueEvent(@NonNull final ContentCaptureEvent event) {
+        enqueueEvent(event, /* forceFlush */ false);
+    }
+
+    /**
+     * Enqueue the event into {@code mEventProcessBuffer} if it is not an urgent request. Otherwise,
+     * clear the buffer events then starting sending out current event.
+     */
+    private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) {
+        if (runOnBackgroundThreadEnabled()) {
+            if (forceFlush) {
+                // The buffer events are cleared in the same thread first to prevent new events
+                // being added during the time of context switch. This would disrupt the sequence
+                // of events.
+                final List<ContentCaptureEvent> batchEvents = clearBufferEvents();
+                runOnContentCaptureThread(() -> {
+                    for (int i = 0; i < batchEvents.size(); i++) {
+                        sendEvent(batchEvents.get(i));
+                    }
+                    sendEvent(event, /* forceFlush= */ true);
+                });
+            } else {
+                mEventProcessQueue.offer(event);
+            }
+        } else {
+            mHandler.post(() -> sendEvent(event, forceFlush));
+        }
     }
 
     /** public because is also used by ViewRootImpl */
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index ac9ad2d..feccc6b 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2320,6 +2320,15 @@
      * @hide
      */
     public boolean hideSoftInputFromView(@NonNull View view, @HideFlags int flags) {
+        final boolean isFocusedAndWindowFocused = view.hasWindowFocus() && view.isFocused();
+        synchronized (mH) {
+            if (!isFocusedAndWindowFocused && !hasServedByInputMethodLocked(view)) {
+                // Fail early if the view is not focused and not served
+                // to avoid logging many erroneous calls.
+                return false;
+            }
+        }
+
         final var reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW;
         final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide(
                 null /* component */, Process.myUid(),
diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java
index c7609a6..828ec26 100644
--- a/core/java/android/webkit/URLUtil.java
+++ b/core/java/android/webkit/URLUtil.java
@@ -44,9 +44,7 @@
     static final String PROXY_BASE = "file:///cookieless_proxy/";
     static final String CONTENT_BASE = "content:";
 
-    /**
-     * Cleans up (if possible) user-entered web addresses
-     */
+    /** Cleans up (if possible) user-entered web addresses */
     public static String guessUrl(String inUrl) {
 
         String retVal = inUrl;
@@ -86,8 +84,12 @@
         return webAddress.toString();
     }
 
-    public static String composeSearchUrl(String inQuery, String template,
-                                          String queryPlaceHolder) {
+    /**
+     * Inserts the {@code inQuery} in the {@code template} after URL-encoding it. The encoded query
+     * will replace the {@code queryPlaceHolder}.
+     */
+    public static String composeSearchUrl(
+            String inQuery, String template, String queryPlaceHolder) {
         int placeHolderIndex = template.indexOf(queryPlaceHolder);
         if (placeHolderIndex < 0) {
             return null;
@@ -104,8 +106,7 @@
             return null;
         }
 
-        buffer.append(template.substring(
-                placeHolderIndex + queryPlaceHolder.length()));
+        buffer.append(template.substring(placeHolderIndex + queryPlaceHolder.length()));
 
         return buffer.toString();
     }
@@ -123,8 +124,7 @@
             byte b = url[i];
             if (b == '%') {
                 if (url.length - i > 2) {
-                    b = (byte) (parseHex(url[i + 1]) * 16
-                            + parseHex(url[i + 2]));
+                    b = (byte) (parseHex(url[i + 1]) * 16 + parseHex(url[i + 2]));
                     i += 2;
                 } else {
                     throw new IllegalArgumentException("Invalid format");
@@ -189,8 +189,8 @@
     }
 
     /**
-     * @return {@code true} if the url is a proxy url to allow cookieless network
-     * requests from a file url.
+     * @return {@code true} if the url is a proxy url to allow cookieless network requests from a
+     *     file url.
      * @deprecated Cookieless proxy is no longer supported.
      */
     @Deprecated
@@ -202,9 +202,10 @@
      * @return {@code true} if the url is a local file.
      */
     public static boolean isFileUrl(String url) {
-        return (null != url) && (url.startsWith(FILE_BASE) &&
-                                 !url.startsWith(ASSET_BASE) &&
-                                 !url.startsWith(PROXY_BASE));
+        return (null != url)
+                && (url.startsWith(FILE_BASE)
+                        && !url.startsWith(ASSET_BASE)
+                        && !url.startsWith(PROXY_BASE));
     }
 
     /**
@@ -232,18 +233,18 @@
      * @return {@code true} if the url is an http: url.
      */
     public static boolean isHttpUrl(String url) {
-        return (null != url) &&
-               (url.length() > 6) &&
-               url.substring(0, 7).equalsIgnoreCase("http://");
+        return (null != url)
+                && (url.length() > 6)
+                && url.substring(0, 7).equalsIgnoreCase("http://");
     }
 
     /**
      * @return {@code true} if the url is an https: url.
      */
     public static boolean isHttpsUrl(String url) {
-        return (null != url) &&
-               (url.length() > 7) &&
-               url.substring(0, 8).equalsIgnoreCase("https://");
+        return (null != url)
+                && (url.length() > 7)
+                && url.substring(0, 8).equalsIgnoreCase("https://");
     }
 
     /**
@@ -271,19 +272,17 @@
             return false;
         }
 
-        return (isAssetUrl(url) ||
-                isResourceUrl(url) ||
-                isFileUrl(url) ||
-                isAboutUrl(url) ||
-                isHttpUrl(url) ||
-                isHttpsUrl(url) ||
-                isJavaScriptUrl(url) ||
-                isContentUrl(url));
+        return (isAssetUrl(url)
+                || isResourceUrl(url)
+                || isFileUrl(url)
+                || isAboutUrl(url)
+                || isHttpUrl(url)
+                || isHttpsUrl(url)
+                || isJavaScriptUrl(url)
+                || isContentUrl(url));
     }
 
-    /**
-     * Strips the url of the anchor.
-     */
+    /** Strips the url of the anchor. */
     public static String stripAnchor(String url) {
         int anchorIndex = url.indexOf('#');
         if (anchorIndex != -1) {
@@ -293,19 +292,16 @@
     }
 
     /**
-     * Guesses canonical filename that a download would have, using
-     * the URL and contentDisposition. File extension, if not defined,
-     * is added based on the mimetype
+     * Guesses canonical filename that a download would have, using the URL and contentDisposition.
+     * File extension, if not defined, is added based on the mimetype
+     *
      * @param url Url to the content
      * @param contentDisposition Content-Disposition HTTP header or {@code null}
      * @param mimeType Mime-type of the content or {@code null}
-     *
      * @return suggested filename
      */
     public static final String guessFileName(
-            String url,
-            @Nullable String contentDisposition,
-            @Nullable String mimeType) {
+            String url, @Nullable String contentDisposition, @Nullable String mimeType) {
         String filename = null;
         String extension = null;
 
@@ -369,8 +365,9 @@
                 // Compare the last segment of the extension against the mime type.
                 // If there's a mismatch, discard the entire extension.
                 int lastDotIndex = filename.lastIndexOf('.');
-                String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
-                        filename.substring(lastDotIndex + 1));
+                String typeFromExt =
+                        MimeTypeMap.getSingleton()
+                                .getMimeTypeFromExtension(filename.substring(lastDotIndex + 1));
                 if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
                     extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
                     if (extension != null) {
@@ -389,17 +386,17 @@
 
     /** Regex used to parse content-disposition headers */
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
-            Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$",
-            Pattern.CASE_INSENSITIVE);
+            Pattern.compile(
+                    "attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$",
+                    Pattern.CASE_INSENSITIVE);
 
     /**
-     * Parse the Content-Disposition HTTP Header. The format of the header
-     * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
-     * This header provides a filename for content that is going to be
-     * downloaded to the file system. We only support the attachment type.
-     * Note that RFC 2616 specifies the filename value must be double-quoted.
-     * Unfortunately some servers do not quote the value so to maintain
-     * consistent behaviour with other browsers, we allow unquoted values too.
+     * Parse the Content-Disposition HTTP Header. The format of the header is defined here:
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
+     * content that is going to be downloaded to the file system. We only support the attachment
+     * type. Note that RFC 2616 specifies the filename value must be double-quoted. Unfortunately
+     * some servers do not quote the value so to maintain consistent behaviour with other browsers,
+     * we allow unquoted values too.
      */
     @UnsupportedAppUsage
     static String parseContentDisposition(String contentDisposition) {
@@ -409,7 +406,7 @@
                 return m.group(2);
             }
         } catch (IllegalStateException ex) {
-             // This function is defined as returning null when it can't parse the header
+            // This function is defined as returning null when it can't parse the header
         }
         return null;
     }
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index 14c5348..d12eda3 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -1203,7 +1203,11 @@
      * changes to this setting after that point.
      *
      * @param flag {@code true} if the WebView should use the database storage API
+     * @deprecated WebSQL is deprecated and this method will become a no-op on all
+     * Android versions once support is removed in Chromium. See
+     * https://developer.chrome.com/blog/deprecating-web-sql for more information.
      */
+    @Deprecated
     public abstract void setDatabaseEnabled(boolean flag);
 
     /**
@@ -1236,7 +1240,11 @@
      *
      * @return {@code true} if the database storage API is enabled
      * @see #setDatabaseEnabled
+     * @deprecated WebSQL is deprecated and this method will become a no-op on all
+     * Android versions once support is removed in Chromium. See
+     * https://developer.chrome.com/blog/deprecating-web-sql for more information.
      */
+    @Deprecated
     public abstract boolean getDatabaseEnabled();
 
     /**
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 8ad10af..0d499a1 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -1014,6 +1014,11 @@
         public int getActionTag() {
             return SET_PENDING_INTENT_TEMPLATE_TAG;
         }
+
+        @Override
+        public void visitUris(@NonNull Consumer<Uri> visitor) {
+            mPendingIntentTemplate.visitUris(visitor);
+        }
     }
 
     /**
@@ -1428,9 +1433,7 @@
 
         @Override
         public void visitUris(@NonNull Consumer<Uri> visitor) {
-            // TODO(b/281044385): Maybe visit intent URIs. This may require adding a dedicated
-            //  visitUris method in the Intent class, since it can contain other intents. Otherwise,
-            //  the basic thing to do here would be just visitor.accept(intent.getData()).
+            mIntent.visitUris(visitor);
         }
     }
 
@@ -1510,7 +1513,7 @@
 
         @Override
         public void visitUris(@NonNull Consumer<Uri> visitor) {
-            // TODO(b/281044385): Maybe visit intent URIs in the RemoteResponse.
+            mResponse.visitUris(visitor);
         }
     }
 
@@ -1559,6 +1562,11 @@
         public int getActionTag() {
             return SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG;
         }
+
+        @Override
+        public void visitUris(@NonNull Consumer<Uri> visitor) {
+            mPendingIntent.visitUris(visitor);
+        }
     }
 
     /**
@@ -1632,7 +1640,7 @@
 
         @Override
         public void visitUris(@NonNull Consumer<Uri> visitor) {
-            // TODO(b/281044385): Maybe visit intent URIs in the RemoteResponse.
+            mResponse.visitUris(visitor);
         }
     }
 
@@ -2193,6 +2201,10 @@
                     final Icon icon = (Icon) getParameterValue(null);
                     if (icon != null) visitIconUri(icon, visitor);
                     break;
+                case INTENT:
+                    final Intent intent = (Intent) getParameterValue(null);
+                    if (intent != null) intent.visitUris(visitor);
+                    break;
                 // TODO(b/281044385): Should we do anything about type BUNDLE?
             }
         }
@@ -6972,6 +6984,20 @@
             mElementNames = parcel.createStringArrayList();
         }
 
+        /**
+         * See {@link RemoteViews#visitUris(Consumer)}.
+         *
+         * @hide
+         */
+        public void visitUris(@NonNull Consumer<Uri> visitor) {
+            if (mPendingIntent != null) {
+                mPendingIntent.visitUris(visitor);
+            }
+            if (mFillIntent != null) {
+                mFillIntent.visitUris(visitor);
+            }
+        }
+
         private void handleViewInteraction(
                 View v,
                 InteractionHandler handler) {
diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java
index c475723..7cda3a3 100644
--- a/core/java/android/window/BackMotionEvent.java
+++ b/core/java/android/window/BackMotionEvent.java
@@ -36,6 +36,7 @@
     private final float mProgress;
     private final float mVelocityX;
     private final float mVelocityY;
+    private final boolean mTriggerBack;
 
     @BackEvent.SwipeEdge
     private final int mSwipeEdge;
@@ -54,6 +55,7 @@
      *                  Value in pixels/second. {@link Float#NaN} if was not computed.
      * @param velocityY Y velocity computed from the touch point of this event.
      *                  Value in pixels/second. {@link Float#NaN} if was not computed.
+     * @param triggerBack Indicates whether the back arrow is in the triggered state or not
      * @param swipeEdge Indicates which edge the swipe starts from.
      * @param departingAnimationTarget The remote animation target of the departing
      *                                 application window.
@@ -64,6 +66,7 @@
             float progress,
             float velocityX,
             float velocityY,
+            boolean triggerBack,
             @BackEvent.SwipeEdge int swipeEdge,
             @Nullable RemoteAnimationTarget departingAnimationTarget) {
         mTouchX = touchX;
@@ -71,6 +74,7 @@
         mProgress = progress;
         mVelocityX = velocityX;
         mVelocityY = velocityY;
+        mTriggerBack = triggerBack;
         mSwipeEdge = swipeEdge;
         mDepartingAnimationTarget = departingAnimationTarget;
     }
@@ -81,6 +85,7 @@
         mProgress = in.readFloat();
         mVelocityX = in.readFloat();
         mVelocityY = in.readFloat();
+        mTriggerBack = in.readBoolean();
         mSwipeEdge = in.readInt();
         mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
     }
@@ -110,6 +115,7 @@
         dest.writeFloat(mProgress);
         dest.writeFloat(mVelocityX);
         dest.writeFloat(mVelocityY);
+        dest.writeBoolean(mTriggerBack);
         dest.writeInt(mSwipeEdge);
         dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
@@ -157,6 +163,15 @@
     }
 
     /**
+     * Returns whether the back arrow is in the triggered state or not
+     *
+     * @return boolean indicating whether the back arrow is in the triggered state or not
+     */
+    public boolean getTriggerBack() {
+        return mTriggerBack;
+    }
+
+    /**
      * Returns the screen edge that the swipe starts from.
      */
     @BackEvent.SwipeEdge
@@ -182,6 +197,7 @@
                 + ", mProgress=" + mProgress
                 + ", mVelocityX=" + mVelocityX
                 + ", mVelocityY=" + mVelocityY
+                + ", mTriggerBack=" + mTriggerBack
                 + ", mSwipeEdge" + mSwipeEdge
                 + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
                 + "}";
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 5bfa3d7..7c9340e 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -165,6 +165,9 @@
     public static final int FLAGS_IS_NON_APP_WINDOW =
             FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD | FLAG_IS_SYSTEM_WINDOW;
 
+    /** The change will not participate in the animation. */
+    public static final int FLAGS_IS_OCCLUDED_NO_ANIMATION = FLAG_IS_OCCLUDED | FLAG_NO_ANIMATION;
+
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
             FLAG_NONE,
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 727bff4..4b5595f 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -23,3 +23,23 @@
   bug: "297550533"
   is_fixed_read_only: true
 }
+
+flag {
+  name: "app_compat_properties_api"
+  namespace: "large_screen_experiences_app_compat"
+  description: "Whether app compat property APIs are public. Which includes: /n"
+                 "WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE,/n"
+                 "WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED,/n"
+                 "WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES,/n"
+                 "WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE,/n"
+                 "WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
+  bug: "316139088"
+  is_fixed_read_only: true
+}
+
+flag {
+  name: "user_min_aspect_ratio_app_default"
+  namespace: "large_screen_experiences_app_compat"
+  description: "Whether the API PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT is available"
+  bug: "310816437"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 07beb11..216acdc 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -15,6 +15,14 @@
 }
 
 flag {
+  name: "enforce_edge_to_edge"
+  namespace: "windowing_frontend"
+  description: "Make app go edge-to-edge when targeting SDK level 35 or greater"
+  bug: "309578419"
+  is_fixed_read_only: true
+}
+
+flag {
     name: "defer_display_updates"
     namespace: "windowing_frontend"
     description: "Feature flag for deferring DisplayManager updates to WindowManager if Shell transition is running"
@@ -58,4 +66,12 @@
     description: "Predictive back for system animations"
     bug: "309545085"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "activity_snapshot_by_default"
+    namespace: "systemui"
+    description: "Enable record activity snapshot by default"
+    bug: "259497289"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 7e2c017..86d3037 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -24,7 +24,9 @@
 import static android.content.ContentProvider.getUserIdFromUri;
 import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
 import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+
 import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.animation.Animator;
@@ -2868,7 +2870,6 @@
     @Override
     public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
         setupScrollListener();
-        maybeSetupGlobalLayoutListener();
 
         ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
         if (chooserListAdapter.getUserHandle()
@@ -2970,28 +2971,6 @@
                 });
     }
 
-    private void maybeSetupGlobalLayoutListener() {
-        if (shouldShowTabs()) {
-            return;
-        }
-        final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
-        recyclerView.getViewTreeObserver()
-                .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        // Fixes an issue were the accessibility border disappears on list creation.
-                        recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                        final TextView titleView = findViewById(R.id.title);
-                        if (titleView != null) {
-                            titleView.setFocusable(true);
-                            titleView.setFocusableInTouchMode(true);
-                            titleView.requestFocus();
-                            titleView.requestAccessibilityFocus();
-                        }
-                    }
-                });
-    }
-
     @Override // ChooserListCommunicator
     public boolean isSendAction(Intent targetIntent) {
         if (targetIntent == null) {
diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java
index efc1455..467cd49 100644
--- a/core/java/com/android/internal/app/SuspendedAppActivity.java
+++ b/core/java/com/android/internal/app/SuspendedAppActivity.java
@@ -39,6 +39,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.SuspendDialogInfo;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
@@ -308,7 +309,8 @@
                         try {
                             final String[] errored = ipm.setPackagesSuspendedAsUser(
                                     new String[]{mSuspendedPackage}, false, null, null, null, 0,
-                                    mSuspendingPackage, mUserId);
+                                    mSuspendingPackage, mUserId /* suspendingUserId */,
+                                    mUserId /* targetUserId */);
                             if (ArrayUtils.contains(errored, mSuspendedPackage)) {
                                 Slog.e(TAG, "Could not unsuspend " + mSuspendedPackage);
                                 break;
@@ -350,17 +352,19 @@
     }
 
     public static Intent createSuspendedAppInterceptIntent(String suspendedPackage,
-            String suspendingPackage, SuspendDialogInfo dialogInfo, Bundle options,
+            UserPackage suspendingPackage, SuspendDialogInfo dialogInfo, Bundle options,
             IntentSender onUnsuspend, int userId) {
-        return new Intent()
+        Intent intent = new Intent()
                 .setClassName("android", SuspendedAppActivity.class.getName())
                 .putExtra(EXTRA_SUSPENDED_PACKAGE, suspendedPackage)
                 .putExtra(EXTRA_DIALOG_INFO, dialogInfo)
-                .putExtra(EXTRA_SUSPENDING_PACKAGE, suspendingPackage)
+                .putExtra(EXTRA_SUSPENDING_PACKAGE,
+                        suspendingPackage != null ? suspendingPackage.packageName : null)
                 .putExtra(EXTRA_UNSUSPEND_INTENT, onUnsuspend)
                 .putExtra(EXTRA_ACTIVITY_OPTIONS, options)
                 .putExtra(Intent.EXTRA_USER_ID, userId)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                         | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        return intent;
     }
 }
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 8b1879f..0ec8b74 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -117,8 +117,6 @@
     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_ROW_SWIPE = Cuj.CUJ_NOTIFICATION_SHADE_ROW_SWIPE;
     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE = Cuj.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE;
-    @Deprecated public static final int CUJ_LAUNCHER_APP_CLOSE_TO_HOME = Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME;
-    @Deprecated public static final int CUJ_LAUNCHER_QUICK_SWITCH = Cuj.CUJ_LAUNCHER_QUICK_SWITCH;
     @Deprecated public static final int CUJ_NOTIFICATION_HEADS_UP_APPEAR = Cuj.CUJ_NOTIFICATION_HEADS_UP_APPEAR;
     @Deprecated public static final int CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR = Cuj.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR;
     @Deprecated public static final int CUJ_NOTIFICATION_ADD = Cuj.CUJ_NOTIFICATION_ADD;
diff --git a/core/java/com/android/internal/pm/parsing/IPackageCacher.java b/core/java/com/android/internal/pm/parsing/IPackageCacher.java
new file mode 100644
index 0000000..3e01730
--- /dev/null
+++ b/core/java/com/android/internal/pm/parsing/IPackageCacher.java
@@ -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.internal.pm.parsing;
+
+import com.android.internal.pm.parsing.pkg.ParsedPackage;
+
+import java.io.File;
+
+/** @hide */
+public interface IPackageCacher {
+
+    /**
+     * Returns the cached parse result for {@code packageFile} for parse flags {@code flags},
+     * or {@code null} if no cached result exists.
+     */
+    ParsedPackage getCachedResult(File packageFile, int flags);
+
+    /**
+     * Caches the parse result for {@code packageFile} with flags {@code flags}.
+     */
+    void cacheResult(File packageFile, int flags, ParsedPackage parsed);
+}
diff --git a/services/core/java/com/android/server/pm/parsing/PackageParser2.java b/core/java/com/android/internal/pm/parsing/PackageParser2.java
similarity index 69%
rename from services/core/java/com/android/server/pm/parsing/PackageParser2.java
rename to core/java/com/android/internal/pm/parsing/PackageParser2.java
index b6a08a5..e413293 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageParser2.java
+++ b/core/java/com/android/internal/pm/parsing/PackageParser2.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.server.pm.parsing;
+package com.android.internal.pm.parsing;
 
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.parsing.PackageLite;
 import android.content.pm.parsing.result.ParseInput;
@@ -28,26 +27,20 @@
 import android.content.pm.parsing.result.ParseTypeImpl;
 import android.content.res.TypedArray;
 import android.os.Build;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.permission.PermissionManager;
 import android.util.DisplayMetrics;
 import android.util.Slog;
 
-import com.android.internal.compat.IPlatformCompat;
 import com.android.internal.pm.parsing.pkg.PackageImpl;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.pm.pkg.parsing.ParsingPackage;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.pm.pkg.parsing.ParsingUtils;
 import com.android.internal.util.ArrayUtils;
-import com.android.server.SystemConfig;
-import com.android.server.pm.PackageManagerException;
-import com.android.server.pm.PackageManagerService;
 
 import java.io.File;
 import java.util.List;
-import java.util.Set;
 
 /**
  * The v2 of package parsing for use when parsing is initiated in the server and must
@@ -59,50 +52,6 @@
  */
 public class PackageParser2 implements AutoCloseable {
 
-    /**
-     * For parsing inside the system server but outside of {@link PackageManagerService}.
-     * Generally used for parsing information in an APK that hasn't been installed yet.
-     *
-     * This must be called inside the system process as it relies on {@link ServiceManager}.
-     */
-    @NonNull
-    public static PackageParser2 forParsingFileWithDefaults() {
-        IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(
-                ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
-        return new PackageParser2(null /* separateProcesses */, null /* displayMetrics */,
-                null /* cacheDir */, new Callback() {
-            @Override
-            public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
-                try {
-                    return platformCompat.isChangeEnabled(changeId, appInfo);
-                } catch (Exception e) {
-                    // This shouldn't happen, but assume enforcement if it does
-                    Slog.wtf(TAG, "IPlatformCompat query failed", e);
-                    return true;
-                }
-            }
-
-            @Override
-            public boolean hasFeature(String feature) {
-                // Assume the device doesn't support anything. This will affect permission parsing
-                // and will force <uses-permission/> declarations to include all requiredNotFeature
-                // permissions and exclude all requiredFeature permissions. This mirrors the old
-                // behavior.
-                return false;
-            }
-
-            @Override
-            public Set<String> getHiddenApiWhitelistedApps() {
-                return SystemConfig.getInstance().getHiddenApiWhitelistedApps();
-            }
-
-            @Override
-            public Set<String> getInstallConstraintsAllowlist() {
-                return SystemConfig.getInstance().getInstallConstraintsAllowlist();
-            }
-        });
-    }
-
     private static final String TAG = ParsingUtils.TAG;
 
     private static final boolean LOG_PARSE_TIMINGS = Build.IS_DEBUGGABLE;
@@ -118,12 +67,12 @@
     private final ThreadLocal<ParseTypeImpl> mSharedResult;
 
     @Nullable
-    protected PackageCacher mCacher;
+    protected IPackageCacher mCacher;
 
-    private final ParsingPackageUtils parsingUtils;
+    private final ParsingPackageUtils mParsingUtils;
 
     public PackageParser2(String[] separateProcesses, DisplayMetrics displayMetrics,
-            @Nullable File cacheDir, @NonNull Callback callback) {
+            @Nullable IPackageCacher cacher, @NonNull Callback callback) {
         if (displayMetrics == null) {
             displayMetrics = new DisplayMetrics();
             displayMetrics.setToDefaults();
@@ -134,9 +83,9 @@
         List<PermissionManager.SplitPermissionInfo> splitPermissions = permissionManager
                 .getSplitPermissions();
 
-        mCacher = cacheDir == null ? null : new PackageCacher(cacheDir);
+        mCacher = cacher;
 
-        parsingUtils = new ParsingPackageUtils(separateProcesses, displayMetrics, splitPermissions,
+        mParsingUtils = new ParsingPackageUtils(separateProcesses, displayMetrics, splitPermissions,
                 callback);
 
         ParseInput.Callback enforcementCallback = (changeId, packageName, targetSdkVersion) -> {
@@ -155,7 +104,7 @@
      */
     @AnyThread
     public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches)
-            throws PackageManagerException {
+            throws PackageParserException {
         var files = packageFile.listFiles();
         // Apk directory is directly nested under the current directory
         if (ArrayUtils.size(files) == 1 && files[0].isDirectory()) {
@@ -171,9 +120,9 @@
 
         long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
         ParseInput input = mSharedResult.get().reset();
-        ParseResult<ParsingPackage> result = parsingUtils.parsePackage(input, packageFile, flags);
+        ParseResult<ParsingPackage> result = mParsingUtils.parsePackage(input, packageFile, flags);
         if (result.isError()) {
-            throw new PackageManagerException(result.getErrorCode(), result.getErrorMessage(),
+            throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
                     result.getException());
         }
 
@@ -201,12 +150,12 @@
      */
     @AnyThread
     public ParsedPackage parsePackageFromPackageLite(PackageLite packageLite, int flags)
-            throws PackageManagerException {
+            throws PackageParserException {
         ParseInput input = mSharedResult.get().reset();
-        ParseResult<ParsingPackage> result = parsingUtils.parsePackageFromPackageLite(input,
+        ParseResult<ParsingPackage> result = mParsingUtils.parsePackageFromPackageLite(input,
                 packageLite, flags);
         if (result.isError()) {
-            throw new PackageManagerException(result.getErrorCode(), result.getErrorMessage(),
+            throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
                     result.getException());
         }
         return result.getResult().hideAsParsed();
@@ -226,7 +175,7 @@
         mSharedAppInfo.remove();
     }
 
-    public static abstract class Callback implements ParsingPackageUtils.Callback {
+    public abstract static class Callback implements ParsingPackageUtils.Callback {
 
         @Override
         public final ParsingPackage startParsingPackage(@NonNull String packageName,
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 201b23c..31910ac 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -169,20 +169,12 @@
     private final static int DEFAULT_BACKGROUND_FADE_DURATION_MS = 300;
 
     /**
-     * Makes navigation bar color transparent by default if the target SDK is
-     * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} or above.
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    private static final long NAV_BAR_COLOR_DEFAULT_TRANSPARENT = 232195501L;
-
-    /**
      * Make app go edge-to-edge by default if the target SDK is
      * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} or above.
      */
     @ChangeId
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    private static final long EDGE_TO_EDGE_BY_DEFAULT = 309578419;
+    private static final long ENFORCE_EDGE_TO_EDGE = 309578419;
 
     private static final int CUSTOM_TITLE_COMPATIBLE_FEATURES = DEFAULT_FEATURES |
             (1 << FEATURE_CUSTOM_TITLE) |
@@ -193,9 +185,9 @@
     private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet();
 
     /**
-     * Since which target SDK version this window should be edge-to-edge by default.
+     * Since which target SDK version this window is enforced to go edge-to-edge.
      */
-    private static final int DEFAULT_EDGE_TO_EDGE_SDK_VERSION =
+    private static final int ENFORCE_EDGE_TO_EDGE_SDK_VERSION =
             SystemProperties.getInt("persist.wm.debug.default_e2e_since_sdk", Integer.MAX_VALUE);
 
     /**
@@ -376,7 +368,7 @@
     boolean mDecorFitsSystemWindows = true;
 
     @VisibleForTesting
-    public final boolean mDefaultEdgeToEdge;
+    public final boolean mEdgeToEdgeEnforced;
 
     private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher;
 
@@ -396,11 +388,11 @@
         mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
         mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
-        mDefaultEdgeToEdge =
-                context.getApplicationInfo().targetSdkVersion >= DEFAULT_EDGE_TO_EDGE_SDK_VERSION
-                        || (CompatChanges.isChangeEnabled(EDGE_TO_EDGE_BY_DEFAULT)
-                                && Flags.edgeToEdgeByDefault());
-        if (mDefaultEdgeToEdge) {
+        mEdgeToEdgeEnforced =
+                context.getApplicationInfo().targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
+                        || (CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
+                                && Flags.enforceEdgeToEdge());
+        if (mEdgeToEdgeEnforced) {
             mDecorFitsSystemWindows = false;
         }
     }
@@ -2472,7 +2464,7 @@
             setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
             params.setFitInsetsSides(0);
             params.setFitInsetsTypes(0);
-            if (mDefaultEdgeToEdge) {
+            if (mEdgeToEdgeEnforced) {
                 params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
             }
         }
@@ -2562,7 +2554,7 @@
             final int statusBarColor = a.getColor(R.styleable.Window_statusBarColor,
                     statusBarDefaultColor);
 
-            mStatusBarColor = statusBarColor == statusBarDefaultColor && !mDefaultEdgeToEdge
+            mStatusBarColor = statusBarColor == statusBarDefaultColor && !mEdgeToEdgeEnforced
                     ? statusBarCompatibleColor
                     : statusBarColor;
         }
@@ -2574,9 +2566,7 @@
 
             mNavigationBarColor =
                     navBarColor == navBarDefaultColor
-                            && !mDefaultEdgeToEdge
-                            && !(CompatChanges.isChangeEnabled(NAV_BAR_COLOR_DEFAULT_TRANSPARENT)
-                                    && Flags.navBarTransparentByDefault())
+                            && !mEdgeToEdgeEnforced
                             && !context.getResources().getBoolean(
                                     R.bool.config_navBarDefaultTransparent)
                     ? navBarCompatibleColor
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index c24d21d..17aad43 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -653,6 +653,8 @@
             sizeof("-Xps-save-resolved-classes-delay-ms:")-1 + PROPERTY_VALUE_MAX];
     char profileMinSavePeriodOptsBuf[sizeof("-Xps-min-save-period-ms:")-1 + PROPERTY_VALUE_MAX];
     char profileMinFirstSaveOptsBuf[sizeof("-Xps-min-first-save-ms:") - 1 + PROPERTY_VALUE_MAX];
+    char profileInlineCacheThresholdOptsBuf[
+            sizeof("-Xps-inline-cache-threshold:") - 1 + PROPERTY_VALUE_MAX];
     char madviseWillNeedFileSizeVdex[
             sizeof("-XMadviseWillNeedVdexFileSize:")-1 + PROPERTY_VALUE_MAX];
     char madviseWillNeedFileSizeOdex[
@@ -898,6 +900,9 @@
     parseRuntimeOption("dalvik.vm.ps-min-first-save-ms", profileMinFirstSaveOptsBuf,
             "-Xps-min-first-save-ms:");
 
+    parseRuntimeOption("dalvik.vm.ps-inline-cache-threshold", profileInlineCacheThresholdOptsBuf,
+            "-Xps-inline-cache-threshold:");
+
     property_get("ro.config.low_ram", propBuf, "");
     if (strcmp(propBuf, "true") == 0) {
       addOption("-XX:LowMemoryMode");
diff --git a/core/jni/android_os_HidlSupport.cpp b/core/jni/android_os_HidlSupport.cpp
index e3602d8..3e51e93 100644
--- a/core/jni/android_os_HidlSupport.cpp
+++ b/core/jni/android_os_HidlSupport.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <hidl/HidlTransportSupport.h>
+#include <hidl/ServiceManagement.h>
 #include <nativehelper/JNIHelp.h>
 
 #include "core_jni_helpers.h"
@@ -24,8 +25,13 @@
     return android::hardware::details::getPidIfSharable();
 }
 
+static jboolean android_os_HidlSupport_isHidlSupported(JNIEnv*, jclass) {
+    return android::hardware::isHidlSupported();
+}
+
 static const JNINativeMethod gHidlSupportMethods[] = {
-    {"getPidIfSharable", "()I", (void*)android_os_HidlSupport_getPidIfSharable},
+        {"getPidIfSharable", "()I", (void*)android_os_HidlSupport_getPidIfSharable},
+        {"isHidlSupported", "()Z", (void*)android_os_HidlSupport_isHidlSupported},
 };
 
 const char* const kHidlSupportPathName = "android/os/HidlSupport";
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 55100a5..7af69f2 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1262,7 +1262,7 @@
     return killProcessGroup(uid, pid, SIGKILL);
 }
 
-jint android_os_Process_sendSignalToProcessGroup(JNIEnv* env, jobject clazz, jint uid, jint pid,
+jboolean android_os_Process_sendSignalToProcessGroup(JNIEnv* env, jobject clazz, jint uid, jint pid,
                                                  jint signal) {
     if (uid < 0) {
         return jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
@@ -1344,7 +1344,7 @@
         //{"setApplicationObject", "(Landroid/os/IBinder;)V",
         //(void*)android_os_Process_setApplicationObject},
         {"killProcessGroup", "(II)I", (void*)android_os_Process_killProcessGroup},
-        {"sendSignalToProcessGroup", "(III)I", (void*)android_os_Process_sendSignalToProcessGroup},
+        {"sendSignalToProcessGroup", "(III)Z", (void*)android_os_Process_sendSignalToProcessGroup},
         {"removeAllProcessGroups", "()V", (void*)android_os_Process_removeAllProcessGroups},
         {"nativePidFdOpen", "(II)I", (void*)android_os_Process_nativePidFdOpen},
         {"freezeCgroupUid", "(IZ)V", (void*)android_os_Process_freezeCgroupUID},
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index fef8ad7..f007cc5 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -41,6 +41,7 @@
     jmethodID dispatchHotplugConnectionError;
     jmethodID dispatchModeChanged;
     jmethodID dispatchFrameRateOverrides;
+    jmethodID dispatchHdcpLevelsChanged;
 
     struct {
         jclass clazz;
@@ -96,6 +97,8 @@
     void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId,
                                     std::vector<FrameRateOverride> overrides) override;
     void dispatchNullEvent(nsecs_t timestamp, PhysicalDisplayId displayId) override {}
+    void dispatchHdcpLevelsChanged(PhysicalDisplayId displayId, int connectedLevel,
+                                   int maxLevel) override;
 };
 
 NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
@@ -294,6 +297,22 @@
     mMessageQueue->raiseAndClearException(env, "dispatchModeChanged");
 }
 
+void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId,
+                                                           int connectedLevel, int maxLevel) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+
+    ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal));
+    if (receiverObj.get()) {
+        ALOGV("receiver %p ~ Invoking hdcp levels changed handler.", this);
+        env->CallVoidMethod(receiverObj.get(),
+                            gDisplayEventReceiverClassInfo.dispatchHdcpLevelsChanged,
+                            displayId.value, connectedLevel, maxLevel);
+        ALOGV("receiver %p ~ Returned from hdcp levels changed handler.", this);
+    }
+
+    mMessageQueue->raiseAndClearException(env, "dispatchHdcpLevelsChanged");
+}
+
 static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject vsyncEventDataWeak,
                         jobject messageQueueObj, jint vsyncSource, jint eventRegistration,
                         jlong layerHandle) {
@@ -385,6 +404,9 @@
             GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz,
                              "dispatchFrameRateOverrides",
                              "(JJ[Landroid/view/DisplayEventReceiver$FrameRateOverride;)V");
+    gDisplayEventReceiverClassInfo.dispatchHdcpLevelsChanged =
+            GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchHdcpLevelsChanged",
+                             "(JII)V");
 
     jclass frameRateOverrideClazz =
             FindClassOrDie(env, "android/view/DisplayEventReceiver$FrameRateOverride");
diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index 052e2f2..d3f3af7 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -610,6 +610,9 @@
     // ringer mode.
     optional SettingProto mode_ringer = 75 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
+    // This is an optional feature where ringer mode affects alarm stream as well
+    optional SettingProto mute_alarm_stream_with_ringer_mode = 160 [ (android.privacy).dest = DEST_AUTOMATIC ];
+
     reserved 147; // Used to be apply_ramping_ringer
 
     message MultiSim {
@@ -1086,5 +1089,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 160;
+    // Next tag = 161;
 }
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 9ca1849..104c023 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -93,7 +93,7 @@
         optional SettingProto hearing_aid_ringtone_routing = 46 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto hearing_aid_call_routing = 47 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto hearing_aid_media_routing = 48 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto hearing_aid_system_sounds_routing = 49 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto hearing_aid_notification_routing = 49 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_magnification_joystick_enabled = 50 [ (android.privacy).dest = DEST_AUTOMATIC ];
         // Settings for font scaling
         optional SettingProto accessibility_font_scaling_has_been_changed = 51 [ (android.privacy).dest = DEST_AUTOMATIC ];
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index c5889ba..75cfba0 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -429,6 +429,7 @@
         optional string base_dir = 1;
         optional string res_dir = 2;
         optional string data_dir = 3;
+        optional int32 targetSdkVersion = 4;
     }
     optional AppInfo appinfo = 8;
     optional ProcessRecordProto app = 9;
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index 404fa39..382a82c 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -394,6 +394,7 @@
     optional bool should_force_rotate_for_camera_compat = 39;
     optional bool should_refresh_activity_for_camera_compat = 40;
     optional bool should_refresh_activity_via_pause_for_camera_compat = 41;
+    optional bool should_override_min_aspect_ratio = 42;
 }
 
 /* represents WindowToken */
diff --git a/core/proto/android/view/insetssource.proto b/core/proto/android/view/insetssource.proto
index e6c6d59..118dfc8 100644
--- a/core/proto/android/view/insetssource.proto
+++ b/core/proto/android/view/insetssource.proto
@@ -26,7 +26,7 @@
  * Represents a {@link android.view.InsetsSource} object.
  */
 message InsetsSourceProto {
-    optional string type = 1;
+    optional string type = 1 [deprecated=true];
     optional .android.graphics.RectProto frame = 2;
     optional .android.graphics.RectProto visible_frame = 3;
     optional bool visible = 4;
diff --git a/core/proto/android/view/insetssourceconsumer.proto b/core/proto/android/view/insetssourceconsumer.proto
index a01ad8e..882163f 100644
--- a/core/proto/android/view/insetssourceconsumer.proto
+++ b/core/proto/android/view/insetssourceconsumer.proto
@@ -27,11 +27,12 @@
  * Represents a {@link android.view.InsetsSourceConsumer} object.
  */
 message InsetsSourceConsumerProto {
-    optional string internal_insets_type = 1;
+    optional string internal_insets_type = 1 [deprecated=true];
     optional bool has_window_focus = 2;
     optional bool is_requested_visible = 3;
     optional InsetsSourceControlProto source_control = 4;
     optional .android.graphics.RectProto pending_frame = 5;
     optional .android.graphics.RectProto pending_visible_frame = 6;
     optional int32 animation_state = 7;
+    optional int32 type_number = 8;
 }
\ No newline at end of file
diff --git a/core/proto/android/view/insetssourcecontrol.proto b/core/proto/android/view/insetssourcecontrol.proto
index 3ac3cbf..afab57f 100644
--- a/core/proto/android/view/insetssourcecontrol.proto
+++ b/core/proto/android/view/insetssourcecontrol.proto
@@ -27,7 +27,8 @@
  * Represents a {@link android.view.InsetsSourceControl} object.
  */
 message InsetsSourceControlProto {
-    optional string type = 1;
+    optional string type = 1 [deprecated=true];
     optional .android.graphics.PointProto position = 2;
     optional SurfaceControlProto leash = 3;
+    optional int32 type_number = 4;
 }
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0264fdc..c6209dd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5189,7 +5189,9 @@
          this permission, it must hold the permission and be the active VoiceInteractionService in
          the system.
          {@see Settings.Secure.VOICE_INTERACTION_SERVICE}
-         @hide -->
+         @hide @SystemApi Intended for OEM and system apps.
+         <p>Protection level: signature|privileged
+    -->
     <permission android:name="android.permission.MANAGE_VOICE_KEYPHRASES"
         android:protectionLevel="signature|privileged" />
 
@@ -5198,7 +5200,9 @@
          sound models at any time. This permission should be reserved for system enrollment
          applications detected by {@link android.hardware.soundtrigger.KeyphraseEnrollmentInfo}
          only.
-         @hide <p>Not for use by third-party applications.</p> -->
+         @hide @SystemApi Intended for OEM and system apps.
+         <p>Protection level: signature|privileged
+    -->
     <permission android:name="android.permission.KEYPHRASE_ENROLLMENT_APPLICATION"
         android:protectionLevel="signature|privileged" />
 
@@ -5702,11 +5706,10 @@
                 android:protectionLevel="normal" />
 
     <!-- @hide @FlaggedApi("android.companion.flags.companion_transport_apis")
-         @SystemApi(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES)
          Allows an application to send and receive messages via CDM transports.
     -->
     <permission android:name="android.permission.USE_COMPANION_TRANSPORTS"
-        android:protectionLevel="signature|module" />
+        android:protectionLevel="signature" />
 
     <!-- Allows an application to create new companion device associations.
          @SystemApi
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 596cfe5..d1143c4 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2986,7 +2986,12 @@
              depends on the number of isolated services that an application starts,
              and how much memory those services save by preloading and sharing memory with
              the app zygote. Therefore, it is recommended to measure memory usage under
-             typical workloads to determine whether it makes sense to use this flag. -->
+             typical workloads to determine whether it makes sense to use this flag.
+
+             <p>There is a limit to the number of isolated services that can be spawned from
+                the Application Zygote; the absolute limit is 100, but due to potential
+                delays in service process cleanup, a much safer limit to use in practice is 50.
+             -->
         <attr name="useAppZygote" format="boolean" />
         <!-- If this is a foreground service, specify its category. -->
         <attr name="foregroundServiceType" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d0de5f0..804e9ef 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2262,6 +2262,9 @@
     <!-- The default min volume for the alarm stream -->
     <integer name="config_audio_alarm_min_vol">1</integer>
 
+    <!-- Flag indicating if ringer mode affects alarm stream -->
+    <bool name="config_audio_ringer_mode_affects_alarm_stream">false</bool>
+
     <!-- The default value for whether head tracking for
          spatial audio is enabled for a newly connected audio device -->
     <bool name="config_spatial_audio_head_tracking_enabled_default">false</bool>
@@ -6862,4 +6865,8 @@
 
     <!-- Whether the media player is shown on the quick settings -->
     <bool name="config_quickSettingsShowMediaPlayer">true</bool>
+
+    <!-- Defines suitability of the built-in speaker route.
+         Refer to {@link MediaRoute2Info} to see supported values.  -->
+    <integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3894330..9589fb0 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -288,6 +288,7 @@
   <java-symbol type="integer" name="config_audio_ring_vol_default" />
   <java-symbol type="integer" name="config_audio_ring_vol_steps" />
   <java-symbol type="integer" name="config_audio_alarm_min_vol" />
+  <java-symbol type="bool" name="config_audio_ringer_mode_affects_alarm_stream" />
   <java-symbol type="bool" name="config_spatial_audio_head_tracking_enabled_default" />
   <java-symbol type="bool" name="config_avoidGfxAccel" />
   <java-symbol type="bool" name="config_bluetooth_address_validation" />
@@ -5306,4 +5307,7 @@
   <java-symbol type="bool" name="config_viewBasedRotaryEncoderHapticsEnabled" />
 
   <java-symbol type="bool" name="config_quickSettingsShowMediaPlayer" />
+
+  <!-- Android MediaRouter framework configs. -->
+  <java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" />
 </resources>
diff --git a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java
index 2b4123a..73d7fe9 100644
--- a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java
+++ b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java
@@ -16,6 +16,9 @@
 
 package android.companion;
 
+import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_PING;
+import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PING;
+
 import android.content.Context;
 import android.os.SystemClock;
 import android.test.InstrumentationTestCase;
@@ -36,16 +39,22 @@
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
+/**
+ * Tests that CDM can intake incoming messages in the system data transport and output results.
+ *
+ * Build/Install/Run: atest CompanionTests:SystemDataTransportTest
+ */
 public class SystemDataTransportTest extends InstrumentationTestCase {
     private static final String TAG = "SystemDataTransportTest";
 
     private static final int MESSAGE_INVALID = 0xF00DCAFE;
-
-    private static final int MESSAGE_REQUEST_INVALID = 0x63636363; // ????
-    private static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
-
+    private static final int MESSAGE_ONEWAY_INVALID = 0x43434343; // ++++
     private static final int MESSAGE_RESPONSE_INVALID = 0x33333333; // !!!!
+    private static final int MESSAGE_REQUEST_INVALID = 0x63636363; // ????
+
     private static final int MESSAGE_RESPONSE_SUCCESS = 0x33838567; // !SUC
     private static final int MESSAGE_RESPONSE_FAILURE = 0x33706573; // !FAI
 
@@ -122,8 +131,6 @@
         new Random().nextBytes(blob);
 
         final byte[] input = generatePacket(MESSAGE_REQUEST_PING, /* sequence */ 1, blob);
-        final byte[] expected = generatePacket(MESSAGE_RESPONSE_SUCCESS, /* sequence */ 1, blob);
-        assertTransportBehavior(input, expected);
     }
 
     public void testMultiplePingPing() {
@@ -176,6 +183,43 @@
         testPingHandRolled();
     }
 
+    public void testInvalidOnewayMessages() throws InterruptedException {
+        // Add a callback
+        final CountDownLatch received = new CountDownLatch(1);
+        mCdm.addOnMessageReceivedListener(Runnable::run, MESSAGE_ONEWAY_INVALID,
+                (id, data) -> received.countDown());
+
+        final byte[] input = generatePacket(MESSAGE_ONEWAY_INVALID, /* sequence */ 1);
+        final ByteArrayInputStream in = new ByteArrayInputStream(input);
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mCdm.attachSystemDataTransport(mAssociationId, in, out);
+
+        // Assert that a one-way message was ignored (does not trigger a callback)
+        assertFalse(received.await(5, TimeUnit.SECONDS));
+
+        // There should not be a response to one-way messages
+        assertEquals(0, out.toByteArray().length);
+    }
+
+
+    public void testOnewayMessages() throws InterruptedException {
+        // Add a callback
+        final CountDownLatch received = new CountDownLatch(1);
+        mCdm.addOnMessageReceivedListener(Runnable::run, MESSAGE_ONEWAY_PING,
+                (id, data) -> received.countDown());
+
+        final byte[] input = generatePacket(MESSAGE_ONEWAY_PING, /* sequence */ 1);
+        final ByteArrayInputStream in = new ByteArrayInputStream(input);
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mCdm.attachSystemDataTransport(mAssociationId, in, out);
+
+        // Assert that a one-way message was received
+        assertTrue(received.await(1, TimeUnit.SECONDS));
+
+        // There should not be a response to one-way messages
+        assertEquals(0, out.toByteArray().length);
+    }
+
     public static byte[] concat(byte[]... blobs) {
         int length = 0;
         for (byte[] blob : blobs) {
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 1a3ec27..c058174 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -202,12 +202,14 @@
         "testng",
     ],
     srcs: [
+        "src/android/database/CursorWindowTest.java",
         "src/android/os/**/*.java",
-        "src/com/android/internal/os/**/*.java",
         "src/android/util/**/*.java",
+        "src/com/android/internal/os/**/*.java",
         "src/com/android/internal/os/LongArrayMultiStateCounterTest.java",
         "src/com/android/internal/util/**/*.java",
         "src/com/android/internal/power/EnergyConsumerStatsTest.java",
+
         ":FrameworksCoreTests{.aapt.srcjar}",
         ":FrameworksCoreTests-aidl",
         ":FrameworksCoreTests-helpers",
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 1577d9c1c1..5b0502d 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -1244,29 +1244,33 @@
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconNull_noPictureIconKey() {
+    public void testBigPictureStyle_setExtras_pictureIconNull_pictureIconKeyNull() {
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture((Bitmap) null);
 
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isTrue();
+        final Parcelable pictureIcon = extras.getParcelable(EXTRA_PICTURE_ICON);
+        assertThat(pictureIcon).isNull();
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconNull_noPictureKey() {
+    public void testBigPictureStyle_setExtras_pictureIconNull_pictureKeyNull() {
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture((Bitmap) null);
 
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE)).isTrue();
+        final Parcelable picture = extras.getParcelable(EXTRA_PICTURE);
+        assertThat(picture).isNull();
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconTypeBitmap_noPictureIconKey() {
+    public void testBigPictureStyle_setExtras_pictureIconTypeBitmap_pictureIconKeyNull() {
         Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture(bitmap);
@@ -1274,11 +1278,13 @@
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE_ICON)).isTrue();
+        final Parcelable pictureIcon = extras.getParcelable(EXTRA_PICTURE_ICON);
+        assertThat(pictureIcon).isNull();
     }
 
     @Test
-    public void testBigPictureStyle_setExtras_pictureIconTypeIcon_noPictureKey() {
+    public void testBigPictureStyle_setExtras_pictureIconTypeIcon_pictureKeyNull() {
         Icon icon = Icon.createWithResource(mContext, R.drawable.btn_plus);
         Notification.BigPictureStyle bpStyle = new Notification.BigPictureStyle();
         bpStyle.bigPicture(icon);
@@ -1286,7 +1292,9 @@
         Bundle extras = new Bundle();
         bpStyle.addExtras(extras);
 
-        assertThat(extras.containsKey(EXTRA_PICTURE)).isFalse();
+        assertThat(extras.containsKey(EXTRA_PICTURE)).isTrue();
+        final Parcelable picture = extras.getParcelable(EXTRA_PICTURE);
+        assertThat(picture).isNull();
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
index ba6c8fa..1617eda 100644
--- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
+++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
@@ -16,24 +16,51 @@
 
 package android.content.res
 
-
 import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.util.SparseArray
 import androidx.core.util.forEach
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
 import kotlin.math.ceil
 import kotlin.math.floor
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.lang.IllegalStateException
 import kotlin.random.Random.Default.nextFloat
 
+/**
+ * Unit tests for FontScaleConverterFactory. Note that some similar tests are in
+ * cts/tests/tests/content/src/android/content/res/cts/FontScaleConverterFactoryTest.kt
+ */
 @Presubmit
 @RunWith(AndroidJUnit4::class)
 class FontScaleConverterFactoryTest {
 
+    @get:Rule
+    val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    private lateinit var defaultLookupTables: SparseArray<FontScaleConverter>
+
+    @Before
+    fun setup() {
+        defaultLookupTables = FontScaleConverterFactory.sLookupTables.clone()
+    }
+
+    @After
+    fun teardown() {
+        // Restore the default tables (since some tests will have added extras to the cache)
+        FontScaleConverterFactory.sLookupTables = defaultLookupTables
+    }
+
     @Test
     fun scale200IsTwiceAtSmallSizes() {
         val table = FontScaleConverterFactory.forScale(2F)!!
@@ -61,19 +88,6 @@
         assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(300f)
     }
 
-    @SmallTest
-    fun missingLookupTable110_returnsInterpolated() {
-        val table = FontScaleConverterFactory.forScale(1.1F)!!
-
-        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.1f)
-        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.1f)
-        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(11f)
-        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.1f)
-        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
-        assertThat(table.convertSpToDp(50F)).isLessThan(50f * 1.1f)
-        assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.1f)
-    }
-
     @Test
     fun missingLookupTable199_returnsInterpolated() {
         val table = FontScaleConverterFactory.forScale(1.9999F)!!
@@ -96,21 +110,51 @@
         assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
+    fun missingLookupTable_cachesInterpolated() {
+        val table = FontScaleConverterFactory.forScale(1.6F)!!
+
+        assertThat(FontScaleConverterFactory.sLookupTables.contains((1.6F * 100).toInt())).isTrue()
+        // Double check known existing values
+        assertThat(FontScaleConverterFactory.sLookupTables.contains((1.5F * 100).toInt())).isTrue()
+        assertThat(FontScaleConverterFactory.sLookupTables.contains((1.7F * 100).toInt())).isFalse()
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC)
+    fun missingLookupTablePastEnd_cachesLinear() {
+        val table = FontScaleConverterFactory.forScale(3F)!!
+
+        assertThat(FontScaleConverterFactory.sLookupTables.contains((3F * 100).toInt())).isTrue()
+        // Double check known existing values
+        assertThat(FontScaleConverterFactory.sLookupTables.contains((1.5F * 100).toInt())).isTrue()
+        assertThat(FontScaleConverterFactory.sLookupTables.contains((1.7F * 100).toInt())).isFalse()
+    }
+
     @SmallTest
+    @Test
     fun missingLookupTableNegativeReturnsNull() {
         assertThat(FontScaleConverterFactory.forScale(-1F)).isNull()
     }
 
     @SmallTest
+    @Test
     fun unnecessaryFontScalesReturnsNull() {
         assertThat(FontScaleConverterFactory.forScale(0F)).isNull()
         assertThat(FontScaleConverterFactory.forScale(1F)).isNull()
+        assertThat(FontScaleConverterFactory.forScale(1.1F)).isNull()
         assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull()
     }
 
     @SmallTest
+    @Test
     fun tablesMatchAndAreMonotonicallyIncreasing() {
-        FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable ->
+        FontScaleConverterFactory.sLookupTables.forEach { _, lookupTable ->
+            if (lookupTable !is FontScaleConverterImpl) {
+                throw IllegalStateException("Didn't return a FontScaleConverterImpl")
+            }
+
             assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size)
             assertThat(lookupTable.mToDpValues).isNotEmpty()
 
@@ -123,6 +167,7 @@
     }
 
     @SmallTest
+    @Test
     fun testIsNonLinearFontScalingActive() {
         assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1f)).isFalse()
         assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0f)).isFalse()
@@ -216,7 +261,7 @@
     }
 
     companion object {
-        private const val CONVERSION_TOLERANCE = 0.05f
+        private const val CONVERSION_TOLERANCE = 0.18f
     }
 }
 
diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt
index bfa8c9a..2c61442 100644
--- a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt
+++ b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt
@@ -86,13 +86,13 @@
     }
 
     private fun createTable(vararg pairs: Pair<Float, Float>) =
-        FontScaleConverter(
+        FontScaleConverterImpl(
             pairs.map { it.first }.toFloatArray(),
             pairs.map { it.second }.toFloatArray()
         )
 
     private fun verifyConversionBothWays(
-        table: FontScaleConverter,
+        table: FontScaleConverterImpl,
         expectedDp: Float,
         spToConvert: Float
     ) {
diff --git a/core/tests/coretests/src/android/database/CursorWindowPerformanceTest.java b/core/tests/coretests/src/android/database/CursorWindowPerformanceTest.java
new file mode 100644
index 0000000..c6d3ebf
--- /dev/null
+++ b/core/tests/coretests/src/android/database/CursorWindowPerformanceTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.database;
+
+import android.test.PerformanceTestCase;
+
+import androidx.test.filters.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * <pre>
+ * m -j44 FrameworksCoreTests
+ * adb install -r -g \
+ *      ${ANDROID_PRODUCT_OUT}/testcases/FrameworksCoreTests/arm64/FrameworksCoreTests.apk
+ * adb shell am instrument -r -e perf true \
+ *      -e class 'android.database.CursorWindowPerformanceTest'
+ *      -w 'com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner'
+ * </pre>
+ */
+public class CursorWindowPerformanceTest extends TestCase implements PerformanceTestCase {
+
+    private final CursorWindowTest mTest = new CursorWindowTest();
+
+    @Override
+    public boolean isPerformanceOnly() {
+        return true;
+    }
+
+    // These test can only be run once.
+    @Override
+    public int startPerformance(Intermediates intermediates) {
+        return 1;
+    }
+
+    @SmallTest
+    public void testConstructor_WithName() {
+        mTest.testConstructor_WithName();
+    }
+
+    @SmallTest
+    public void testConstructorWithEmptyName() {
+        mTest.testConstructorWithEmptyName();
+    }
+
+    @SmallTest
+    public void testValues() {
+        mTest.testValues();
+    }
+}
diff --git a/core/tests/coretests/src/android/database/CursorWindowTest.java b/core/tests/coretests/src/android/database/CursorWindowTest.java
index 123da3e..255020a 100644
--- a/core/tests/coretests/src/android/database/CursorWindowTest.java
+++ b/core/tests/coretests/src/android/database/CursorWindowTest.java
@@ -16,25 +16,29 @@
 
 package android.database;
 
-import android.test.PerformanceTestCase;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.database.sqlite.SQLiteException;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 
-import junit.framework.TestCase;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.Arrays;
 
-public class CursorWindowTest extends TestCase implements PerformanceTestCase {
-    public boolean isPerformanceOnly() {
-        return false;
-    }
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CursorWindowTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
 
-    // These test can only be run once.
-    public int startPerformance(Intermediates intermediates) {
-        return 1;
-    }
-
-    @SmallTest
+    @Test
     public void testConstructor_WithName() {
         CursorWindow window = new CursorWindow("MyWindow");
         assertEquals("MyWindow", window.getName());
@@ -42,7 +46,7 @@
         window.close();
     }
 
-    @SmallTest
+    @Test
     public void testConstructorWithEmptyName() {
         CursorWindow window = new CursorWindow("");
         assertEquals("<unnamed>", window.getName());
@@ -50,7 +54,7 @@
         window.close();
     }
 
-    @SmallTest
+    @Test
     public void testConstructorWithNullName() {
         CursorWindow window = new CursorWindow(null);
         assertEquals("<unnamed>", window.getName());
@@ -58,7 +62,7 @@
         window.close();
     }
 
-    @SmallTest
+    @Test
     public void testDeprecatedConstructor() {
         @SuppressWarnings("deprecation")
         CursorWindow window = new CursorWindow(true /*this argument is ignored*/);
@@ -67,7 +71,7 @@
         window.close();
     }
 
-    @SmallTest
+    @Test
     public void testValues() {
         CursorWindow window = new CursorWindow("MyWindow");
         doTestValues(window);
@@ -77,17 +81,25 @@
     private void doTestValues(CursorWindow window) {
         assertTrue(window.setNumColumns(7));
         assertTrue(window.allocRow());
+        assertEquals(window.getType(0, 0), Cursor.FIELD_TYPE_NULL);
+
         double db1 = 1.26;
         assertTrue(window.putDouble(db1, 0, 0));
+        assertEquals(window.getType(0, 0), Cursor.FIELD_TYPE_FLOAT);
         double db2 = window.getDouble(0, 0);
-        assertEquals(db1, db2);
+        assertEquals(db1, db2, 0.01);
+        assertEquals(1, window.getInt(0, 0));
+        assertEquals("1.26", window.getString(0, 0));
+        assertThrows(SQLiteException.class, () -> window.getBlob(0, 0));
 
         long int1 = Long.MAX_VALUE;
         assertTrue(window.putLong(int1, 0, 1));
+        assertEquals(window.getType(0, 1), Cursor.FIELD_TYPE_INTEGER);
         long int2 = window.getLong(0, 1);
         assertEquals(int1, int2);
 
         assertTrue(window.putString("1198032740000", 0, 3));
+        assertEquals(window.getType(0, 3), Cursor.FIELD_TYPE_STRING);
         assertEquals("1198032740000", window.getString(0, 3));
         assertEquals(1198032740000L, window.getLong(0, 3));
 
@@ -97,13 +109,14 @@
         
         assertTrue(window.putString(Double.toString(42.0), 0, 4));
         assertEquals(Double.toString(42.0), window.getString(0, 4));
-        assertEquals(42.0, window.getDouble(0, 4));
+        assertEquals(42.0, window.getDouble(0, 4), 0.01);
         
         // put blob
         byte[] blob = new byte[1000];
         byte value = 99;
         Arrays.fill(blob, value);
         assertTrue(window.putBlob(blob, 0, 6));
+        assertEquals(window.getType(0, 6), Cursor.FIELD_TYPE_BLOB);
         assertTrue(Arrays.equals(blob, window.getBlob(0, 6)));
     }
 }
diff --git a/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java b/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java
new file mode 100644
index 0000000..da3a465
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/face/FaceSensorConfigurationsTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.hardware.face;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.SensorProps;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.function.Function;
+
+@Presubmit
+@SmallTest
+public class FaceSensorConfigurationsTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private IFace mFace;
+    @Mock
+    private Function<String, IFace> mGetIFace;
+
+    private final String[] mAidlInstances = new String[]{"default", "virtual"};
+    private String[] mHidlConfigStrings = new String[]{"0:2:15", "0:8:15"};
+    private FaceSensorConfigurations mFaceSensorConfigurations;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mGetIFace.apply(anyString())).thenReturn(mFace);
+        when(mFace.getSensorProps()).thenReturn(new SensorProps[]{});
+        when(mContext.getResources()).thenReturn(mResources);
+    }
+
+    @Test
+    public void testAidlInstanceSensorProps() {
+        mFaceSensorConfigurations = new FaceSensorConfigurations(false);
+        mFaceSensorConfigurations.addAidlConfigs(mAidlInstances, mGetIFace);
+
+        assertThat(mFaceSensorConfigurations.hasSensorConfigurations()).isTrue();
+        assertThat(!mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge())
+                .isFalse();
+    }
+
+    @Test
+    public void testHidlConfigStrings() {
+        mFaceSensorConfigurations = new FaceSensorConfigurations(true);
+        mFaceSensorConfigurations.addHidlConfigs(mHidlConfigStrings, mContext);
+
+        assertThat(mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge())
+                .isTrue();
+    }
+
+    @Test
+    public void testHidlConfigStrings_incorrectFormat() {
+        mHidlConfigStrings = new String[]{"0:8:15", "0:2", "0:face:15"};
+        mFaceSensorConfigurations = new FaceSensorConfigurations(true);
+        mFaceSensorConfigurations.addHidlConfigs(mHidlConfigStrings, mContext);
+
+        assertThat(mFaceSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFaceSensorConfigurations.getResetLockoutRequiresChallenge())
+                .isTrue();
+    }
+}
diff --git a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java
new file mode 100644
index 0000000..613089c
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintSensorConfigurationsTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.hardware.fingerprint;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.function.Function;
+
+
+@Presubmit
+@SmallTest
+public class FingerprintSensorConfigurationsTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private IFingerprint mFingerprint;
+    @Mock
+    private Function<String, IFingerprint> mGetIFingerprint;
+
+    private final String[] mAidlInstances = new String[]{"default", "virtual"};
+    private String[] mHidlConfigStrings = new String[]{"0:2:15", "0:8:15"};
+    private FingerprintSensorConfigurations mFingerprintSensorConfigurations;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mGetIFingerprint.apply(anyString())).thenReturn(mFingerprint);
+        when(mFingerprint.getSensorProps()).thenReturn(new SensorProps[]{});
+        when(mContext.getResources()).thenReturn(mResources);
+    }
+
+    @Test
+    public void testAidlInstanceSensorProps() {
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                true /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addAidlSensors(mAidlInstances, mGetIFingerprint);
+
+        assertThat(mFingerprintSensorConfigurations.hasSensorConfigurations()).isTrue();
+        assertThat(!mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent())
+                .isTrue();
+        assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())
+                .isTrue();
+    }
+
+    @Test
+    public void testHidlConfigStrings() {
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                false /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addHidlSensors(mHidlConfigStrings, mContext);
+
+        assertThat(mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())
+                .isFalse();
+    }
+
+    @Test
+    public void testHidlConfigStrings_incorrectFormat() {
+        mHidlConfigStrings = new String[]{"0:8:15", "0:2", "0:fingerprint:15"};
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                false /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addHidlSensors(mHidlConfigStrings, mContext);
+
+        assertThat(mFingerprintSensorConfigurations.isSingleSensorConfigurationPresent()).isTrue();
+        assertThat(mFingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken())
+                .isFalse();
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index b30a0c8..cf3eb12 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -659,8 +659,6 @@
         ViewRootImpl viewRootImpl = view.getViewRootImpl();
         sInstrumentation.runOnMainSync(() -> {
             view.invalidate();
-            assertEquals(viewRootImpl.getLastPreferredFrameRateCategory(),
-                    FRAME_RATE_CATEGORY_NORMAL);
             viewRootImpl.notifyInsetsAnimationRunningStateChanged(true);
             view.invalidate();
         });
@@ -672,6 +670,37 @@
         });
     }
 
+
+    /**
+     * Test FrameRateBoostOnTouchEnabled API
+     */
+    @Test
+    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
+    public void votePreferredFrameRate_frameRateBoostOnTouch() {
+        View view = new View(sContext);
+        attachViewToWindow(view);
+        sInstrumentation.waitForIdleSync();
+
+        ViewRootImpl viewRootImpl = view.getViewRootImpl();
+        final WindowManager.LayoutParams attrs = viewRootImpl.mWindowAttributes;
+        assertEquals(attrs.getFrameRateBoostOnTouchEnabled(), true);
+        assertEquals(viewRootImpl.getFrameRateBoostOnTouchEnabled(),
+                attrs.getFrameRateBoostOnTouchEnabled());
+
+        sInstrumentation.runOnMainSync(() -> {
+            attrs.setFrameRateBoostOnTouchEnabled(false);
+            viewRootImpl.setLayoutParams(attrs, false);
+        });
+        sInstrumentation.waitForIdleSync();
+
+        sInstrumentation.runOnMainSync(() -> {
+            final WindowManager.LayoutParams newAttrs = viewRootImpl.mWindowAttributes;
+            assertEquals(newAttrs.getFrameRateBoostOnTouchEnabled(), false);
+            assertEquals(viewRootImpl.getFrameRateBoostOnTouchEnabled(),
+                    newAttrs.getFrameRateBoostOnTouchEnabled());
+        });
+    }
+
     @Test
     public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() {
         mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index c8ea374..15c9047 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -34,9 +34,6 @@
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -837,6 +834,33 @@
     }
 
     @Test
+    public void visitUris_intents() {
+        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
+
+        Uri fillIntentUri = Uri.parse("content://intent/fill");
+        views.setOnCheckedChangeResponse(
+                R.id.layout,
+                RemoteViews.RemoteResponse.fromFillInIntent(new Intent("action", fillIntentUri)));
+
+        Uri pendingIntentUri = Uri.parse("content://intent/pending");
+        PendingIntent pendingIntent = getPendingIntentWithUri(pendingIntentUri);
+        views.setOnClickResponse(
+                R.id.layout,
+                RemoteViews.RemoteResponse.fromPendingIntent(pendingIntent));
+
+        Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+        views.visitUris(visitor);
+        verify(visitor, times(1)).accept(eq(fillIntentUri));
+        verify(visitor, times(1)).accept(eq(pendingIntentUri));
+    }
+
+    private PendingIntent getPendingIntentWithUri(Uri uri) {
+        return PendingIntent.getActivity(mContext, 0,
+                new Intent("action", uri),
+                PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    @Test
     public void layoutInflaterFactory_nothingSet_returnsNull() {
         final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
         assertNull(rv.getLayoutInflaterFactory());
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 68c0693..a709d7b 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -84,6 +84,7 @@
             /* progress = */ 0,
             /* velocityX = */ 0,
             /* velocityY = */ 0,
+            /* triggerBack = */ false,
             /* swipeEdge = */ BackEvent.EDGE_LEFT,
             /* departingAnimationTarget = */ null);
 
diff --git a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
index 6bdc06a..de55b07 100644
--- a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
+++ b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
@@ -63,7 +63,7 @@
         createPhoneWindowWithTheme(R.style.LayoutInDisplayCutoutModeUnset);
         installDecor();
 
-        if (mPhoneWindow.mDefaultEdgeToEdge && !mPhoneWindow.isFloating()) {
+        if (mPhoneWindow.mEdgeToEdgeEnforced && !mPhoneWindow.isFloating()) {
             assertThat(mPhoneWindow.getAttributes().layoutInDisplayCutoutMode,
                     is(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS));
         } else {
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 69a6e6d..c6f920f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -518,6 +518,8 @@
         <permission name="android.permission.ACCESS_BROADCAST_RADIO"/>
         <!-- Permission required for CTS test - CtsAmbientContextServiceTestCases -->
         <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/>
+        <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases -->
+        <permission name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"/>
         <!-- Permission required for CTS test - CtsTelephonyProviderTestCases -->
         <permission name="android.permission.WRITE_APN_SETTINGS"/>
         <!-- Permission required for GTS test - GtsStatsdHostTestCases -->
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index aaddf0e..742d5a2 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -379,12 +379,6 @@
       "group": "WM_DEBUG_IME",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
-    "-1770075711": {
-      "message": "Adding window client %s that is dead, aborting.",
-      "level": "WARN",
-      "group": "WM_ERROR",
-      "at": "com\/android\/server\/wm\/WindowManagerService.java"
-    },
     "-1768557332": {
       "message": "removeWallpaperAnimation()",
       "level": "DEBUG",
@@ -3253,6 +3247,12 @@
       "group": "WM_DEBUG_LOCKTASK",
       "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
     },
+    "723575093": {
+      "message": "Attempted to add window with a client %s that is dead. Aborting.",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "726205185": {
       "message": "Moving to DESTROYED: %s (destroy skipped)",
       "level": "VERBOSE",
@@ -4237,12 +4237,6 @@
       "group": "WM_DEBUG_APP_TRANSITIONS_ANIM",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1720696061": {
-      "message": "Adding window to Display that has been removed.",
-      "level": "WARN",
-      "group": "WM_ERROR",
-      "at": "com\/android\/server\/wm\/WindowManagerService.java"
-    },
     "1730300180": {
       "message": "PendingStartTransaction found",
       "level": "VERBOSE",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java
deleted file mode 100644
index ff49cdc..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java
+++ /dev/null
@@ -1,119 +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 androidx.window.extensions.embedding;
-
-import static java.util.Objects.requireNonNull;
-
-import android.graphics.Rect;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-/**
- * The parameter to create an overlay container that retrieved from
- * {@link android.app.ActivityOptions} bundle.
- */
-class OverlayCreateParams {
-
-    // TODO(b/295803704): Move them to WM Extensions so that we can reuse in WM Jetpack.
-    @VisibleForTesting
-    static final String KEY_OVERLAY_CREATE_PARAMS =
-            "androidx.window.extensions.OverlayCreateParams";
-
-    @VisibleForTesting
-    static final String KEY_OVERLAY_CREATE_PARAMS_TASK_ID =
-            "androidx.window.extensions.OverlayCreateParams.taskId";
-
-    @VisibleForTesting
-    static final String KEY_OVERLAY_CREATE_PARAMS_TAG =
-            "androidx.window.extensions.OverlayCreateParams.tag";
-
-    @VisibleForTesting
-    static final String KEY_OVERLAY_CREATE_PARAMS_BOUNDS =
-            "androidx.window.extensions.OverlayCreateParams.bounds";
-
-    private final int mTaskId;
-
-    @NonNull
-    private final String mTag;
-
-    @NonNull
-    private final Rect mBounds;
-
-    OverlayCreateParams(int taskId, @NonNull String tag, @NonNull Rect bounds) {
-        mTaskId = taskId;
-        mTag = requireNonNull(tag);
-        mBounds = requireNonNull(bounds);
-    }
-
-    int getTaskId() {
-        return mTaskId;
-    }
-
-    @NonNull
-    String getTag() {
-        return mTag;
-    }
-
-    @NonNull
-    Rect getBounds() {
-        return mBounds;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = mTaskId;
-        result = 31 * result + mTag.hashCode();
-        result = 31 * result + mBounds.hashCode();
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) return true;
-        if (!(obj instanceof OverlayCreateParams thatParams)) return false;
-        return mTaskId == thatParams.mTaskId
-                && mTag.equals(thatParams.mTag)
-                && mBounds.equals(thatParams.mBounds);
-    }
-
-    @Override
-    public String toString() {
-        return OverlayCreateParams.class.getSimpleName() + ": {"
-                + "taskId=" + mTaskId
-                + ", tag=" + mTag
-                + ", bounds=" + mBounds
-                + "}";
-    }
-
-    /** Retrieves the {@link OverlayCreateParams} from {@link android.app.ActivityOptions} bundle */
-    @Nullable
-    static OverlayCreateParams fromBundle(@NonNull Bundle bundle) {
-        final Bundle paramsBundle = bundle.getBundle(KEY_OVERLAY_CREATE_PARAMS);
-        if (paramsBundle == null) {
-            return null;
-        }
-        final int taskId = paramsBundle.getInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID);
-        final String tag = requireNonNull(paramsBundle.getString(KEY_OVERLAY_CREATE_PARAMS_TAG));
-        final Rect bounds = requireNonNull(paramsBundle.getParcelable(
-                KEY_OVERLAY_CREATE_PARAMS_BOUNDS, Rect.class));
-
-        return new OverlayCreateParams(taskId, tag, bounds);
-    }
-}
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 4973a4d..15ee4e1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -34,6 +34,7 @@
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
 
+import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG;
 import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
 import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
 import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
@@ -136,6 +137,15 @@
     private Function<SplitAttributesCalculatorParams, SplitAttributes> mSplitAttributesCalculator;
 
     /**
+     * A calculator function to compute {@link ActivityStack} attributes in a task, which is called
+     * when there's {@link #onTaskFragmentParentInfoChanged} or folding state changed.
+     */
+    @GuardedBy("mLock")
+    @Nullable
+    private Function<ActivityStackAttributesCalculatorParams, ActivityStackAttributes>
+            mActivityStackAttributesCalculator;
+
+    /**
      * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info
      * below it.
      * When the app is host of multiple Tasks, there can be multiple splits controlled by the same
@@ -319,6 +329,22 @@
         }
     }
 
+    @Override
+    public void setActivityStackAttributesCalculator(
+            @NonNull Function<ActivityStackAttributesCalculatorParams, ActivityStackAttributes>
+                    calculator) {
+        synchronized (mLock) {
+            mActivityStackAttributesCalculator = calculator;
+        }
+    }
+
+    @Override
+    public void clearActivityStackAttributesCalculator() {
+        synchronized (mLock) {
+            mActivityStackAttributesCalculator = null;
+        }
+    }
+
     @GuardedBy("mLock")
     @Nullable
     Function<SplitAttributesCalculatorParams, SplitAttributes> getSplitAttributesCalculator() {
@@ -1412,7 +1438,7 @@
             @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
             @Nullable Activity launchingActivity) {
         return createEmptyContainer(wct, intent, taskId, new Rect(), launchingActivity,
-                null /* overlayTag */);
+                null /* overlayTag */, null /* launchOptions */);
     }
 
     /**
@@ -1426,7 +1452,7 @@
     TaskFragmentContainer createEmptyContainer(
             @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
             @NonNull Rect bounds, @Nullable Activity launchingActivity,
-            @Nullable String overlayTag) {
+            @Nullable String overlayTag, @Nullable Bundle launchOptions) {
         // We need an activity in the organizer process in the same Task to use as the owner
         // activity, as well as to get the Task window info.
         final Activity activityInTask;
@@ -1443,7 +1469,8 @@
             return null;
         }
         final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */,
-                intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag);
+                intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag,
+                launchOptions);
         final IBinder taskFragmentToken = container.getTaskFragmentToken();
         // Note that taskContainer will not exist before calling #newContainer if the container
         // is the first embedded TF in the task.
@@ -1570,14 +1597,16 @@
     TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity,
             @NonNull Activity activityInTask, int taskId) {
         return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */,
-                activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */);
+                activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */,
+                null /* launchOptions */);
     }
 
     @GuardedBy("mLock")
     TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
             @NonNull Activity activityInTask, int taskId) {
         return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
-                activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */);
+                activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */,
+                null /* launchOptions */);
     }
 
     @GuardedBy("mLock")
@@ -1585,7 +1614,8 @@
                                        @NonNull Activity activityInTask, int taskId,
                                        @NonNull TaskFragmentContainer pairedPrimaryContainer) {
         return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
-                activityInTask, taskId, pairedPrimaryContainer, null /* tag */);
+                activityInTask, taskId, pairedPrimaryContainer, null /* tag */,
+                null /* launchOptions */);
     }
 
     /**
@@ -1602,11 +1632,14 @@
      * @param overlayTag                The tag for the new created overlay container. It must be
      *                                  needed if {@code isOverlay} is {@code true}. Otherwise,
      *                                  it should be {@code null}.
+     * @param launchOptions             The launch options bundle to create a container. Must be
+     *                                  specified for overlay container.
      */
     @GuardedBy("mLock")
     TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
             @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId,
-            @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) {
+            @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag,
+            @Nullable Bundle launchOptions) {
         if (activityInTask == null) {
             throw new IllegalArgumentException("activityInTask must not be null,");
         }
@@ -1615,7 +1648,8 @@
         }
         final TaskContainer taskContainer = mTaskContainers.get(taskId);
         final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
-                pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag);
+                pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag,
+                launchOptions);
         return container;
     }
 
@@ -2345,28 +2379,28 @@
     @GuardedBy("mLock")
     @Nullable
     TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
-            @NonNull WindowContainerTransaction wct,
-            @NonNull OverlayCreateParams overlayCreateParams, int launchTaskId,
+            @NonNull WindowContainerTransaction wct, @NonNull Bundle options,
             @NonNull Intent intent, @NonNull Activity launchActivity) {
-        final int taskId = overlayCreateParams.getTaskId();
-        if (taskId != launchTaskId) {
-            // The task ID doesn't match the launch activity's. Cannot determine the host task
-            // to launch the overlay.
-            throw new IllegalArgumentException("The task ID of "
-                    + "OverlayCreateParams#launchingActivity must match the task ID of "
-                    + "the activity to #startActivity with the activity options that takes "
-                    + "OverlayCreateParams.");
-        }
         final List<TaskFragmentContainer> overlayContainers =
                 getAllOverlayTaskFragmentContainers();
-        final String overlayTag = overlayCreateParams.getTag();
+        final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG));
 
         // If the requested bounds of OverlayCreateParams are smaller than minimum dimensions
         // specified by Intent, expand the overlay container to fill the parent task instead.
-        final Rect bounds = overlayCreateParams.getBounds();
-        final Size minDimensions = getMinDimensions(intent);
-        final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(bounds,
-                minDimensions);
+        final ActivityStackAttributesCalculatorParams params =
+                new ActivityStackAttributesCalculatorParams(mPresenter.toParentContainerInfo(
+                        mPresenter.getTaskProperties(launchActivity)), overlayTag, options);
+        // Fallback to expand the bounds if there's no activityStackAttributes calculator.
+        final Rect relativeBounds = mActivityStackAttributesCalculator != null
+                ? new Rect(mActivityStackAttributesCalculator.apply(params).getRelativeBounds())
+                : new Rect();
+        final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(relativeBounds,
+                getMinDimensions(intent));
+        // Expand the bounds if the requested bounds are smaller than minimum dimensions.
+        if (shouldExpandContainer) {
+            relativeBounds.setEmpty();
+        }
+        final int taskId = getTaskId(launchActivity);
         if (!overlayContainers.isEmpty()) {
             for (final TaskFragmentContainer overlayContainer : overlayContainers) {
                 if (!overlayTag.equals(overlayContainer.getOverlayTag())
@@ -2390,7 +2424,7 @@
                     final Rect taskBounds = overlayContainer.getTaskContainer().getTaskProperties()
                             .getTaskMetrics().getBounds();
                     final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
-                    final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds);
+                    final Rect sanitizedBounds = sanitizeBounds(relativeBounds, intent, taskBounds);
                     mPresenter.resizeTaskFragment(wct, overlayToken, sanitizedBounds);
                     mPresenter.setTaskFragmentIsolatedNavigation(wct, overlayToken,
                             !sanitizedBounds.isEmpty());
@@ -2402,8 +2436,9 @@
                 }
             }
         }
-        return createEmptyContainer(wct, intent, taskId,
-                (shouldExpandContainer ? new Rect() : bounds), launchActivity, overlayTag);
+        // Launch the overlay container to the task with taskId.
+        return createEmptyContainer(wct, intent, taskId, relativeBounds, launchActivity, overlayTag,
+                options);
     }
 
     private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
@@ -2568,12 +2603,11 @@
                 final TaskFragmentContainer launchedInTaskFragment;
                 if (launchingActivity != null) {
                     final int taskId = getTaskId(launchingActivity);
-                    final OverlayCreateParams overlayCreateParams =
-                            OverlayCreateParams.fromBundle(options);
+                    final String overlayTag = options.getString(KEY_OVERLAY_TAG);
                     if (Flags.activityEmbeddingOverlayPresentationFlag()
-                            && overlayCreateParams != null) {
+                            && overlayTag != null) {
                         launchedInTaskFragment = createOrUpdateOverlayTaskFragmentIfNeeded(wct,
-                                overlayCreateParams, taskId, intent, launchingActivity);
+                                options, intent, launchingActivity);
                     } else {
                         launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
                                 launchingActivity);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index b5c32bb..acfd8e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -1084,4 +1084,14 @@
     WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) {
         return getTaskProperties(activity).getTaskMetrics();
     }
+
+    @NonNull
+    ParentContainerInfo toParentContainerInfo(@NonNull TaskProperties taskProperties) {
+        final Configuration configuration = taskProperties.getConfiguration();
+        final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent
+                .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
+                        configuration.windowConfiguration);
+        return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration,
+                windowLayoutInfo);
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 3e7f99b..afd554b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -24,6 +24,7 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Size;
 import android.window.TaskFragmentAnimationParams;
@@ -105,6 +106,13 @@
     @Nullable
     private final String mOverlayTag;
 
+    /**
+     * The launch options that was used to create this container. Must not be {@code null} for
+     * {@link #isOverlay()} container.
+     */
+    @Nullable
+    private final Bundle mLaunchOptions;
+
     /** Indicates whether the container was cleaned up after the last activity was removed. */
     private boolean mIsFinished;
 
@@ -165,7 +173,7 @@
 
     /**
      * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController,
-     * TaskFragmentContainer, String)
+     * TaskFragmentContainer, String, Bundle)
      */
     TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
                           @Nullable Intent pendingAppearedIntent,
@@ -173,7 +181,8 @@
                           @NonNull SplitController controller,
                           @Nullable TaskFragmentContainer pairedPrimaryContainer) {
         this(pendingAppearedActivity, pendingAppearedIntent, taskContainer,
-                controller, pairedPrimaryContainer, null /* overlayTag */);
+                controller, pairedPrimaryContainer, null /* overlayTag */,
+                null /* launchOptions */);
     }
 
     /**
@@ -181,11 +190,14 @@
      * container transaction.
      * @param pairedPrimaryContainer    when it is set, the new container will be add right above it
      * @param overlayTag                Sets to indicate this taskFragment is an overlay container
+     * @param launchOptions             The launch options to create this container. Must not be
+     *                                  {@code null} for an overlay container
      */
     TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
             @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer,
             @NonNull SplitController controller,
-            @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) {
+            @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag,
+            @Nullable Bundle launchOptions) {
         if ((pendingAppearedActivity == null && pendingAppearedIntent == null)
                 || (pendingAppearedActivity != null && pendingAppearedIntent != null)) {
             throw new IllegalArgumentException(
@@ -195,6 +207,10 @@
         mToken = new Binder("TaskFragmentContainer");
         mTaskContainer = taskContainer;
         mOverlayTag = overlayTag;
+        if (overlayTag != null) {
+            Objects.requireNonNull(launchOptions);
+        }
+        mLaunchOptions = launchOptions;
 
         if (pairedPrimaryContainer != null) {
             // The TaskFragment will be positioned right above the paired container.
@@ -344,7 +360,7 @@
         if (activities == null) {
             return null;
         }
-        return new ActivityStack(activities, isEmpty(), mToken);
+        return new ActivityStack(activities, isEmpty(), mToken, mOverlayTag);
     }
 
     /** Adds the activity that will be reparented to this container. */
@@ -901,8 +917,8 @@
     }
 
     /**
-     * Returns the tag specified in {@link OverlayCreateParams#getTag()}. {@code null} if this
-     * taskFragment container is not an overlay container.
+     * Returns the tag specified in launch options. {@code null} if this taskFragment container is
+     * not an overlay container.
      */
     @Nullable
     String getOverlayTag() {
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 4c2433f..678bdef 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
@@ -18,14 +18,11 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
-import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS;
-import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_BOUNDS;
-import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_TAG;
-import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_TASK_ID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -45,6 +42,7 @@
 import static org.mockito.Mockito.never;
 
 import android.app.Activity;
+import android.app.ActivityOptions;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -98,9 +96,6 @@
     @Rule
     public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
 
-    private static final OverlayCreateParams TEST_OVERLAY_CREATE_PARAMS =
-            new OverlayCreateParams(TASK_ID, "test,", new Rect(0, 0, 200, 200));
-
     private SplitController.ActivityStartMonitor mMonitor;
 
     private Intent mIntent;
@@ -165,37 +160,15 @@
     }
 
     @Test
-    public void testOverlayCreateParamsFromBundle() {
-        assertThat(OverlayCreateParams.fromBundle(new Bundle())).isNull();
-
-        assertThat(OverlayCreateParams.fromBundle(createOverlayCreateParamsTestBundle()))
-                .isEqualTo(TEST_OVERLAY_CREATE_PARAMS);
-    }
-
-    @Test
     public void testStartActivity_overlayFeatureDisabled_notInvokeCreateOverlayContainer() {
         mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG);
 
-        mMonitor.onStartActivity(mActivity, mIntent, createOverlayCreateParamsTestBundle());
+        final Bundle optionsBundle = ActivityOptions.makeBasic().toBundle();
+        optionsBundle.putString(KEY_OVERLAY_TAG, "test");
+        mMonitor.onStartActivity(mActivity, mIntent, optionsBundle);
 
         verify(mSplitController, never()).createOrUpdateOverlayTaskFragmentIfNeeded(any(), any(),
-                anyInt(), any(), any());
-    }
-
-    @NonNull
-    private static Bundle createOverlayCreateParamsTestBundle() {
-        final Bundle bundle = new Bundle();
-
-        final Bundle paramsBundle = new Bundle();
-        paramsBundle.putInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID,
-                TEST_OVERLAY_CREATE_PARAMS.getTaskId());
-        paramsBundle.putString(KEY_OVERLAY_CREATE_PARAMS_TAG, TEST_OVERLAY_CREATE_PARAMS.getTag());
-        paramsBundle.putObject(KEY_OVERLAY_CREATE_PARAMS_BOUNDS,
-                TEST_OVERLAY_CREATE_PARAMS.getBounds());
-
-        bundle.putBundle(KEY_OVERLAY_CREATE_PARAMS, paramsBundle);
-
-        return bundle;
+                any(), any());
     }
 
     @Test
@@ -221,19 +194,11 @@
     }
 
     @Test
-    public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_taskIdNotMatch_throwException() {
-        assertThrows("The method must return null due to task mismatch between"
-                + " launchingActivity and OverlayCreateParams", IllegalArgumentException.class,
-                () -> createOrUpdateOverlayTaskFragmentIfNeeded(
-                        TEST_OVERLAY_CREATE_PARAMS, TASK_ID + 1));
-    }
-
-    @Test
     public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask_dismissOverlay() {
         createExistingOverlayContainers();
 
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                new OverlayCreateParams(TASK_ID, "test3", new Rect(0, 0, 100, 100)), TASK_ID);
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateOverlayTaskFragmentIfNeeded("test3");
 
         assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
                 + " is launched to the same task")
@@ -245,9 +210,9 @@
     public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_sameTagAnotherTask_dismissOverlay() {
         createExistingOverlayContainers();
 
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                new OverlayCreateParams(TASK_ID + 2, "test1", new Rect(0, 0, 100, 100)),
-                TASK_ID + 2);
+        doReturn(TASK_ID + 2).when(mActivity).getTaskId();
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateOverlayTaskFragmentIfNeeded("test1");
 
         assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
                 + " is launched with the same tag as an existing overlay container in a different "
@@ -261,9 +226,10 @@
         createExistingOverlayContainers();
 
         final Rect bounds = new Rect(0, 0, 100, 100);
+        mSplitController.setActivityStackAttributesCalculator(params ->
+                new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
         final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                new OverlayCreateParams(TASK_ID, "test1", bounds),
-                TASK_ID);
+                "test1");
 
         assertWithMessage("overlayContainer1 must be updated since the new overlay container"
                 + " is launched with the same tag and task")
@@ -279,9 +245,8 @@
     public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissMultipleOverlays() {
         createExistingOverlayContainers();
 
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                new OverlayCreateParams(TASK_ID, "test2", new Rect(0, 0, 100, 100)),
-                TASK_ID);
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateOverlayTaskFragmentIfNeeded("test2");
 
         // OverlayContainer1 is dismissed since new container is launched in the same task with
         // different tag. OverlayContainer2 is dismissed since new container is launched with the
@@ -304,8 +269,11 @@
         mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(),
                 MinimumDimensionActivity.class));
 
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                TEST_OVERLAY_CREATE_PARAMS, TASK_ID);
+        final Rect bounds = new Rect(0, 0, 100, 100);
+        mSplitController.setActivityStackAttributesCalculator(params ->
+                new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateOverlayTaskFragmentIfNeeded("test");
         final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
 
         assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
@@ -316,7 +284,7 @@
 
         // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case.
         clearInvocations(mSplitPresenter);
-        createOrUpdateOverlayTaskFragmentIfNeeded(TEST_OVERLAY_CREATE_PARAMS, TASK_ID);
+        createOrUpdateOverlayTaskFragmentIfNeeded("test");
 
         verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect());
         verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
@@ -329,11 +297,11 @@
     public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_notInTaskBounds_expandOverlay() {
         final Rect bounds = new Rect(TASK_BOUNDS);
         bounds.offset(10, 10);
-        final OverlayCreateParams paramsOutsideTaskBounds = new OverlayCreateParams(TASK_ID,
-                "test", bounds);
+        mSplitController.setActivityStackAttributesCalculator(params ->
+                new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
 
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                paramsOutsideTaskBounds, TASK_ID);
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateOverlayTaskFragmentIfNeeded("test");
         final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
 
         assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
@@ -344,7 +312,7 @@
 
         // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case.
         clearInvocations(mSplitPresenter);
-        createOrUpdateOverlayTaskFragmentIfNeeded(paramsOutsideTaskBounds, TASK_ID);
+        createOrUpdateOverlayTaskFragmentIfNeeded("test");
 
         verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect());
         verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
@@ -355,15 +323,17 @@
 
     @Test
     public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_createOverlay() {
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                TEST_OVERLAY_CREATE_PARAMS, TASK_ID);
+        final Rect bounds = new Rect(0, 0, 100, 100);
+        mSplitController.setActivityStackAttributesCalculator(params ->
+                new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateOverlayTaskFragmentIfNeeded("test");
 
         assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
                 .containsExactly(overlayContainer);
         assertThat(overlayContainer.getTaskId()).isEqualTo(TASK_ID);
-        assertThat(overlayContainer
-                .areLastRequestedBoundsEqual(TEST_OVERLAY_CREATE_PARAMS.getBounds())).isTrue();
-        assertThat(overlayContainer.getOverlayTag()).isEqualTo(TEST_OVERLAY_CREATE_PARAMS.getTag());
+        assertThat(overlayContainer.areLastRequestedBoundsEqual(bounds)).isTrue();
+        assertThat(overlayContainer.getOverlayTag()).isEqualTo("test");
     }
 
     @Test
@@ -416,12 +386,14 @@
 
     @Test
     public void testGetTopNonFinishingActivityWithOverlay() {
-        createTestOverlayContainer(TASK_ID, "test1");
+        TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1");
+
         final Activity activity = createMockActivity();
         final TaskFragmentContainer container = createMockTaskFragmentContainer(activity);
         final TaskContainer task = container.getTaskContainer();
 
-        assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)).isEqualTo(mActivity);
+        assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */))
+                .isEqualTo(overlayContainer.getTopNonFinishingActivity());
         assertThat(task.getTopNonFinishingActivity(false /* includeOverlay */)).isEqualTo(activity);
     }
 
@@ -458,10 +430,11 @@
      * #createOrUpdateOverlayTaskFragmentIfNeeded}
      */
     @Nullable
-    private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
-            @NonNull OverlayCreateParams params, int taskId) {
-        return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, params,
-                taskId, mIntent, mActivity);
+    private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(@NonNull String tag) {
+        final Bundle launchOptions = new Bundle();
+        launchOptions.putString(KEY_OVERLAY_TAG, tag);
+        return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction,
+                launchOptions, mIntent, mActivity);
     }
 
     /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
@@ -475,10 +448,11 @@
 
     @NonNull
     private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) {
+        Activity activity = createMockActivity();
         TaskFragmentContainer overlayContainer = mSplitController.newContainer(
-                null /* pendingAppearedActivity */, mIntent, mActivity, taskId,
-                null /* pairedPrimaryContainer */, tag);
-        setupTaskFragmentInfo(overlayContainer, mActivity);
+                null /* pendingAppearedActivity */, mIntent, activity, taskId,
+                null /* pairedPrimaryContainer */, tag, Bundle.EMPTY);
+        setupTaskFragmentInfo(overlayContainer, activity);
         return overlayContainer;
     }
 
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 8c274a2..bab4e91 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
@@ -590,7 +590,7 @@
 
         assertFalse(result);
         verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
-                anyString());
+                anyString(), any());
     }
 
     @Test
@@ -753,7 +753,7 @@
 
         assertTrue(result);
         verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
-                anyString());
+                anyString(), any());
         verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
     }
 
@@ -796,7 +796,7 @@
 
         assertTrue(result);
         verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
-                anyString());
+                anyString(), any());
         verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
     }
 
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 4511f3b..901d5fa 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -57,3 +57,10 @@
     description: "Enables left/right split in portrait"
     bug: "291018646"
 }
+
+flag {
+    name: "enable_new_bubble_animations"
+    namespace: "multitasking"
+    description: "Enables new animations for expand and collapse for bubbles"
+    bug: "311450609"
+}
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 85bf2c1..e4f793c 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
@@ -30,8 +30,8 @@
         android:orientation="horizontal"
         android:clickable="true"
         android:focusable="true"
-        android:paddingStart="16dp">
-
+        android:paddingStart="6dp"
+        android:paddingEnd="8dp">
         <ImageView
             android:id="@+id/application_icon"
             android:layout_width="@dimen/desktop_mode_caption_icon_radius"
@@ -43,7 +43,7 @@
             android:id="@+id/application_name"
             android:layout_width="0dp"
             android:layout_height="20dp"
-            android:minWidth="80dp"
+            android:maxWidth="86dp"
             android:textAppearance="@android:style/TextAppearance.Material.Title"
             android:textSize="14sp"
             android:textFontWeight="500"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 8f9de61..0a40cea 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -413,6 +413,25 @@
     <!-- Height of desktop mode caption for fullscreen tasks. -->
     <dimen name="desktop_mode_fullscreen_decor_caption_height">36dp</dimen>
 
+    <!-- Required empty space to be visible for partially offscreen tasks. -->
+    <dimen name="freeform_required_visible_empty_space_in_header">48dp</dimen>
+
+    <!-- Required empty space to be visible for partially offscreen tasks on a smaller screen. -->
+    <dimen name="small_screen_required_visible_empty_space_in_header">12dp</dimen>
+
+    <!-- 32dp width back button + 10dp margin -->
+    <dimen name="caption_left_buttons_width">32dp</dimen>
+
+    <!-- (32 dp buttons + 10dp margins) * 3 buttons-->
+    <dimen name="caption_right_buttons_width">126dp</dimen>
+
+    <!-- 2 buttons * 48dp button size. -->
+    <dimen name="desktop_mode_right_edge_buttons_width">96dp</dimen>
+
+    <!-- 22dp padding + 24dp app icon + 16dp expand button.
+         Text varies in size, we will calculate that width separately. -->
+    <dimen name="desktop_mode_app_details_width_minus_text">62dp</dimen>
+
     <!-- The width of the maximize menu in desktop mode. -->
     <dimen name="desktop_mode_maximize_menu_width">287dp</dimen>
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
index e06d3ef..5b0de50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java
@@ -21,5 +21,4 @@
  */
 class BackAnimationConstants {
     static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f;
-    static final float PROGRESS_COMMIT_THRESHOLD = 0.1f;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index d8c691b..a498236 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -70,9 +70,11 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
+import java.io.PrintWriter;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -124,6 +126,7 @@
     private final Context mContext;
     private final ContentResolver mContentResolver;
     private final ShellController mShellController;
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
 
@@ -180,7 +183,8 @@
             @NonNull @ShellBackgroundThread Handler backgroundHandler,
             Context context,
             @NonNull BackAnimationBackground backAnimationBackground,
-            ShellBackAnimationRegistry shellBackAnimationRegistry) {
+            ShellBackAnimationRegistry shellBackAnimationRegistry,
+            ShellCommandHandler shellCommandHandler) {
         this(
                 shellInit,
                 shellController,
@@ -190,7 +194,8 @@
                 context,
                 context.getContentResolver(),
                 backAnimationBackground,
-                shellBackAnimationRegistry);
+                shellBackAnimationRegistry,
+                shellCommandHandler);
     }
 
     @VisibleForTesting
@@ -203,7 +208,8 @@
             Context context,
             ContentResolver contentResolver,
             @NonNull BackAnimationBackground backAnimationBackground,
-            ShellBackAnimationRegistry shellBackAnimationRegistry) {
+            ShellBackAnimationRegistry shellBackAnimationRegistry,
+            ShellCommandHandler shellCommandHandler) {
         mShellController = shellController;
         mShellExecutor = shellExecutor;
         mActivityTaskManager = activityTaskManager;
@@ -219,6 +225,7 @@
                 .build();
         mShellBackAnimationRegistry = shellBackAnimationRegistry;
         mLatencyTracker = LatencyTracker.getInstance(mContext);
+        mShellCommandHandler = shellCommandHandler;
     }
 
     private void onInit() {
@@ -227,6 +234,7 @@
         createAdapter();
         mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
                 this::createExternalInterface, this);
+        mShellCommandHandler.addDumpCallback(this::dump, this);
     }
 
     private void setupAnimationDeveloperSettingsObserver(
@@ -968,4 +976,20 @@
                 };
         mBackAnimationAdapter = new BackAnimationAdapter(runner);
     }
+
+    /**
+     * Description of current BackAnimationController state.
+     */
+    private void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "BackAnimationController state:");
+        pw.println(prefix + "  mEnableAnimations=" + mEnableAnimations.get());
+        pw.println(prefix + "  mBackGestureStarted=" + mBackGestureStarted);
+        pw.println(prefix + "  mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress);
+        pw.println(prefix + "  mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent);
+        pw.println(prefix + "  mCurrentTracker state:");
+        mCurrentTracker.dump(pw, prefix + "    ");
+        pw.println(prefix + "  mQueuedTracker state:");
+        mQueuedTracker.dump(pw, prefix + "    ");
+    }
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
index 215a6cc..30d5edb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
@@ -18,9 +18,9 @@
 
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.window.BackEvent.EDGE_RIGHT;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD;
 import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
 
@@ -91,7 +91,7 @@
                 }
             };
     private static final float MIN_WINDOW_ALPHA = 0.01f;
-    private static final float WINDOW_X_SHIFT_DP = 96;
+    private static final float WINDOW_X_SHIFT_DP = 48;
     private static final int SCALE_FACTOR = 100;
     // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
     private static final float TARGET_COMMIT_PROGRESS = 0.5f;
@@ -126,6 +126,8 @@
     private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
 
     private boolean mBackInProgress = false;
+    private boolean mIsRightEdge;
+    private boolean mTriggerBack = false;
 
     private PointF mTouchPos = new PointF();
     private IRemoteAnimationFinishedCallback mFinishCallback;
@@ -209,6 +211,7 @@
 
     private void finishAnimation() {
         if (mEnteringTarget != null) {
+            mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
             mEnteringTarget.leash.release();
             mEnteringTarget = null;
         }
@@ -241,14 +244,15 @@
 
     private void onGestureProgress(@NonNull BackEvent backEvent) {
         if (!mBackInProgress) {
+            mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
             mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
             mBackInProgress = true;
         }
         mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
 
         float progress = backEvent.getProgress();
-        float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD
-                ? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1)
+        float springProgress = (mTriggerBack
+                ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
                 : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
         mLeavingProgressSpring.animateToFinalPosition(springProgress);
         mEnteringProgressSpring.animateToFinalPosition(springProgress);
@@ -312,7 +316,7 @@
             transformWithProgress(
                     mEnteringProgress,
                     Math.max(
-                            smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress),
+                            smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
                             MIN_WINDOW_ALPHA),  /* alpha */
                     mEnteringTarget.leash,
                     mEnteringRect,
@@ -337,14 +341,13 @@
                     mClosingTarget.leash,
                     mClosingRect,
                     0,
-                    mWindowXShift
+                    mIsRightEdge ? 0 : mWindowXShift
             );
         }
     }
 
     private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
             RectF targetRect, float deltaXMin, float deltaXMax) {
-        final float touchY = mTouchPos.y;
 
         final int width = mStartTaskRect.width();
         final int height = mStartTaskRect.height();
@@ -376,12 +379,14 @@
     private final class Callback extends IOnBackInvokedCallback.Default {
         @Override
         public void onBackStarted(BackMotionEvent backEvent) {
+            mTriggerBack = backEvent.getTriggerBack();
             mProgressAnimator.onBackStarted(backEvent,
                     CrossActivityBackAnimation.this::onGestureProgress);
         }
 
         @Override
         public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+            mTriggerBack = backEvent.getTriggerBack();
             mProgressAnimator.onBackProgressed(backEvent);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
index 4bd56d4..6213f62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -24,6 +24,8 @@
 import android.window.BackEvent;
 import android.window.BackMotionEvent;
 
+import java.io.PrintWriter;
+
 /**
  * Helper class to record the touch location for gesture and generate back events.
  */
@@ -129,6 +131,7 @@
                 /* progress = */ 0,
                 /* velocityX = */ 0,
                 /* velocityY = */ 0,
+                /* triggerBack = */ mTriggerBack,
                 /* swipeEdge = */ mSwipeEdge,
                 /* departingAnimationTarget = */ target);
     }
@@ -204,6 +207,7 @@
                 /* progress = */ progress,
                 /* velocityX = */ mLatestVelocityX,
                 /* velocityY = */ mLatestVelocityY,
+                /* triggerBack = */ mTriggerBack,
                 /* swipeEdge = */ mSwipeEdge,
                 /* departingAnimationTarget = */ null);
     }
@@ -219,6 +223,12 @@
         mNonLinearFactor = nonLinearFactor;
     }
 
+    void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "TouchTracker state:");
+        pw.println(prefix + "  mState=" + mState);
+        pw.println(prefix + "  mTriggerBack=" + mTriggerBack);
+    }
+
     enum TouchTrackerState {
         INITIAL, ACTIVE, FINISHED
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt
index e57f02c..bd47082 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt
@@ -40,7 +40,13 @@
 
     /** Whether education view should show for the collapsed stack. */
     fun shouldShowStackEducation(bubble: BubbleViewProvider?): Boolean {
-        val shouldShow = bubble != null &&
+        if (BubbleDebugConfig.neverShowUserEducation(context)) {
+            logDebug("Show stack edu: never")
+            return false
+        }
+
+        val shouldShow =
+            bubble != null &&
                 bubble.isConversationBubble && // show education for conversation bubbles only
                 (!hasSeenStackEducation || BubbleDebugConfig.forceShowUserEducation(context))
         logDebug("Show stack edu: $shouldShow")
@@ -49,7 +55,13 @@
 
     /** Whether the educational view should show for the expanded view "manage" menu. */
     fun shouldShowManageEducation(bubble: BubbleViewProvider?): Boolean {
-        val shouldShow = bubble != null &&
+        if (BubbleDebugConfig.neverShowUserEducation(context)) {
+            logDebug("Show manage edu: never")
+            return false
+        }
+
+        val shouldShow =
+            bubble != null &&
                 bubble.isConversationBubble && // show education for conversation bubbles only
                 (!hasSeenManageEducation || BubbleDebugConfig.forceShowUserEducation(context))
         logDebug("Show manage edu: $shouldShow")
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 b7f749e..470a825 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
@@ -1291,7 +1291,7 @@
             // We only show user education for conversation bubbles right now
             return false;
         }
-        final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION);
+        final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION);
         final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
                 && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
@@ -1342,7 +1342,7 @@
             // We only show user education for conversation bubbles right now
             return false;
         }
-        final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION);
+        final boolean seen = getPrefBoolean(StackEducationView.PREF_STACK_EDUCATION);
         final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext);
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
             Log.d(TAG, "Show stack edu: " + shouldShow);
@@ -2323,7 +2323,8 @@
         updateOverflowVisibility();
         updatePointerPosition(false /* forIme */);
         mExpandedAnimationController.expandFromStack(() -> {
-            if (mIsExpanded && mExpandedBubble.getExpandedView() != null) {
+            if (mIsExpanded && mExpandedBubble != null
+                    && mExpandedBubble.getExpandedView() != null) {
                 maybeShowManageEdu();
             }
             updateOverflowDotVisibility(true /* expanding */);
@@ -2384,7 +2385,7 @@
         }
         mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
 
-        if (mExpandedBubble.getExpandedView() != null) {
+        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
             mExpandedBubble.getExpandedView().setContentAlpha(0f);
             mExpandedBubble.getExpandedView().setBackgroundAlpha(0f);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
index 61e17c8..da71b1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt
@@ -33,15 +33,16 @@
  * User education view to highlight the manage button that allows a user to configure the settings
  * for the bubble. Shown only the first time a user expands a bubble.
  */
-class ManageEducationView(context: Context, positioner: BubblePositioner) : LinearLayout(context) {
+class ManageEducationView(
+    context: Context,
+    private val positioner: BubblePositioner
+) : LinearLayout(context) {
 
-    private val TAG =
-        if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView"
-        else BubbleDebugConfig.TAG_BUBBLES
+    companion object {
+        const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
+        private const val ANIMATE_DURATION: Long = 200
+    }
 
-    private val ANIMATE_DURATION: Long = 200
-
-    private val positioner: BubblePositioner = positioner
     private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) }
     private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) }
     private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) }
@@ -128,7 +129,7 @@
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .alpha(1f)
         }
-        setShouldShow(false)
+        updateManageEducationSeen()
     }
 
     /**
@@ -218,13 +219,11 @@
             }
     }
 
-    private fun setShouldShow(shouldShow: Boolean) {
+    private fun updateManageEducationSeen() {
         context
             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
             .edit()
-            .putBoolean(PREF_MANAGED_EDUCATION, !shouldShow)
+            .putBoolean(PREF_MANAGED_EDUCATION, true)
             .apply()
     }
 }
-
-const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 2cabb65..95f1017 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -34,19 +34,15 @@
  */
 class StackEducationView(
     context: Context,
-    positioner: BubblePositioner,
-    controller: BubbleController
+    private val positioner: BubblePositioner,
+    private val controller: BubbleController
 ) : LinearLayout(context) {
 
-    private val TAG =
-        if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView"
-        else BubbleDebugConfig.TAG_BUBBLES
-
-    private val ANIMATE_DURATION: Long = 200
-    private val ANIMATE_DURATION_SHORT: Long = 40
-
-    private val positioner: BubblePositioner = positioner
-    private val controller: BubbleController = controller
+    companion object {
+        const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
+        private const val ANIMATE_DURATION: Long = 200
+        private const val ANIMATE_DURATION_SHORT: Long = 40
+    }
 
     private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
     private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
@@ -175,7 +171,7 @@
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .alpha(1f)
         }
-        setShouldShow(false)
+        updateStackEducationSeen()
         return true
     }
 
@@ -196,13 +192,11 @@
             .withEndAction { visibility = GONE }
     }
 
-    private fun setShouldShow(shouldShow: Boolean) {
+    private fun updateStackEducationSeen() {
         context
             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
             .edit()
-            .putBoolean(PREF_STACK_EDUCATION, !shouldShow)
+            .putBoolean(PREF_STACK_EDUCATION, true)
             .apply()
     }
 }
-
-const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index 5b0239f..02af2d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -563,7 +563,7 @@
                         ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
                         : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
                 animationForChild(child)
-                        .translationX(fromX, p.y)
+                        .translationX(fromX, p.x)
                         .start();
             } else {
                 float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
@@ -634,4 +634,9 @@
                     .start();
         }
     }
+
+    /** Returns true if we're in the middle of a collapse or expand animation. */
+    boolean isAnimating() {
+        return mAnimatingCollapse || mAnimatingExpand;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 7f34ee0..893a87f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -23,6 +23,8 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.util.Log;
+import android.util.Size;
+import android.view.View;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
@@ -33,6 +35,7 @@
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject.MagneticTarget;
 
 /**
  * Helper class to animate a {@link BubbleBarExpandedView} on a bubble.
@@ -44,6 +47,15 @@
     private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f;
     private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f;
     private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
+    private static final int EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION = 100;
+    private static final int EXPANDED_VIEW_ANIMATE_POSITION_DURATION = 300;
+    private static final int EXPANDED_VIEW_DISMISS_DURATION = 250;
+    private static final int EXPANDED_VIEW_DRAG_ANIMATION_DURATION = 150;
+    /**
+     * Additional scale applied to expanded view when it is positioned inside a magnetic target.
+     */
+    private static final float EXPANDED_VIEW_IN_TARGET_SCALE = 0.6f;
+    private static final float EXPANDED_VIEW_DRAG_SCALE = 0.5f;
 
     /** Spring config for the expanded view scale-in animation. */
     private final PhysicsAnimator.SpringConfig mScaleInSpringConfig =
@@ -62,6 +74,7 @@
     private final Context mContext;
     private final BubbleBarLayerView mLayerView;
     private final BubblePositioner mPositioner;
+    private final int[] mTmpLocation = new int[2];
 
     private BubbleViewProvider mExpandedBubble;
     private boolean mIsExpanded = false;
@@ -181,7 +194,8 @@
             Log.w(TAG, "Trying to animate collapse without a bubble");
             return;
         }
-
+        bbev.setScaleX(1f);
+        bbev.setScaleY(1f);
         mExpandedViewContainerMatrix.setScaleX(1f);
         mExpandedViewContainerMatrix.setScaleY(1f);
 
@@ -209,11 +223,173 @@
     }
 
     /**
+     * Animate the expanded bubble when it is being dragged
+     */
+    public void animateStartDrag() {
+        final BubbleBarExpandedView bbev = getExpandedView();
+        if (bbev == null) {
+            Log.w(TAG, "Trying to animate start drag without a bubble");
+            return;
+        }
+        bbev.setPivotX(bbev.getWidth() / 2f);
+        bbev.setPivotY(0f);
+        bbev.animate()
+                .scaleX(EXPANDED_VIEW_DRAG_SCALE)
+                .scaleY(EXPANDED_VIEW_DRAG_SCALE)
+                .setInterpolator(Interpolators.EMPHASIZED)
+                .setDuration(EXPANDED_VIEW_DRAG_ANIMATION_DURATION)
+                .start();
+    }
+
+    /**
+     * Animates dismissal of currently expanded bubble
+     *
+     * @param endRunnable a runnable to run at the end of the animation
+     */
+    public void animateDismiss(Runnable endRunnable) {
+        mIsExpanded = false;
+        final BubbleBarExpandedView bbev = getExpandedView();
+        if (bbev == null) {
+            Log.w(TAG, "Trying to animate dismiss without a bubble");
+            return;
+        }
+
+        int[] location = bbev.getLocationOnScreen();
+        int diffFromBottom = mPositioner.getScreenRect().bottom - location[1];
+
+        bbev.animate()
+                // 2x distance from bottom so the view flies out
+                .translationYBy(diffFromBottom * 2)
+                .setDuration(EXPANDED_VIEW_DISMISS_DURATION)
+                .withEndAction(endRunnable)
+                .start();
+    }
+
+    /**
+     * Animate current expanded bubble back to its rest position
+     */
+    public void animateToRestPosition() {
+        BubbleBarExpandedView bbev = getExpandedView();
+        if (bbev == null) {
+            Log.w(TAG, "Trying to animate expanded view to rest position without a bubble");
+            return;
+        }
+        Point restPoint = getExpandedViewRestPosition(getExpandedViewSize());
+        bbev.animate()
+                .x(restPoint.x)
+                .y(restPoint.y)
+                .scaleX(1f)
+                .scaleY(1f)
+                .setDuration(EXPANDED_VIEW_ANIMATE_POSITION_DURATION)
+                .setInterpolator(Interpolators.EMPHASIZED_DECELERATE)
+                .withStartAction(() -> bbev.setAnimating(true))
+                .withEndAction(() -> {
+                    bbev.setAnimating(false);
+                    bbev.resetPivot();
+                })
+                .start();
+    }
+
+    /**
+     * Animates currently expanded bubble into the given {@link MagneticTarget}.
+     *
+     * @param target magnetic target to snap to
+     * @param endRunnable a runnable to run at the end of the animation
+     */
+    public void animateIntoTarget(MagneticTarget target, @Nullable Runnable endRunnable) {
+        BubbleBarExpandedView bbev = getExpandedView();
+        if (bbev == null) {
+            Log.w(TAG, "Trying to snap the expanded view to target without a bubble");
+            return;
+        }
+
+        // Calculate scale of expanded view so it fits inside the magnetic target
+        float bbevMaxSide = Math.max(bbev.getWidth(), bbev.getHeight());
+        View targetView = target.getTargetView();
+        float targetMaxSide = Math.max(targetView.getWidth(), targetView.getHeight());
+        // Reduce target size to have some padding between the target and expanded view
+        targetMaxSide *= EXPANDED_VIEW_IN_TARGET_SCALE;
+        float scaleInTarget = targetMaxSide / bbevMaxSide;
+
+        // Scale around the top center of the expanded view. Same as when dragging.
+        bbev.setPivotX(bbev.getWidth() / 2f);
+        bbev.setPivotY(0);
+
+        // When the view animates into the target, it is scaled down with the pivot at center top.
+        // Find the point on the view that would be the center of the view at its final scale.
+        // Once we know that, we can calculate x and y distance from the center of the target view
+        // and use that for the translation animation to ensure that the view at final scale is
+        // placed at the center of the target.
+
+        // Set mTmpLocation to the current location of the view on the screen, taking into account
+        // any scale applied.
+        bbev.getLocationOnScreen(mTmpLocation);
+        // Since pivotX is at the center of the x-axis, even at final scale, center of the view on
+        // x-axis will be the same as the center of the view at current size.
+        // Get scaled width of the view and adjust mTmpLocation so that point on x-axis is at the
+        // center of the view at its current size.
+        float currentWidth = bbev.getWidth() * bbev.getScaleX();
+        mTmpLocation[0] += currentWidth / 2;
+        // Since pivotY is at the top of the view, at final scale, top coordinate of the view
+        // remains the same.
+        // Get height of the view at final scale and adjust mTmpLocation so that point on y-axis is
+        // moved down by half of the height at final scale.
+        float targetHeight = bbev.getHeight() * scaleInTarget;
+        mTmpLocation[1] += targetHeight / 2;
+        // mTmpLocation is now set to the point on the view that will be the center of the view once
+        // scale is applied.
+
+        // Calculate the difference between the target's center coordinates and mTmpLocation
+        float xDiff = target.getCenterOnScreen().x - mTmpLocation[0];
+        float yDiff = target.getCenterOnScreen().y - mTmpLocation[1];
+
+        bbev.animate()
+                .translationX(bbev.getTranslationX() + xDiff)
+                .translationY(bbev.getTranslationY() + yDiff)
+                .scaleX(scaleInTarget)
+                .scaleY(scaleInTarget)
+                .setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION)
+                .setInterpolator(Interpolators.EMPHASIZED)
+                .withStartAction(() -> bbev.setAnimating(true))
+                .withEndAction(() -> {
+                    bbev.setAnimating(false);
+                    if (endRunnable != null) {
+                        endRunnable.run();
+                    }
+                })
+                .start();
+    }
+
+    /**
+     * Animate currently expanded view when it is released from dismiss view
+     */
+    public void animateUnstuckFromDismissView() {
+        BubbleBarExpandedView expandedView = getExpandedView();
+        if (expandedView == null) {
+            Log.w(TAG, "Trying to unsnap the expanded view from dismiss without a bubble");
+            return;
+        }
+        expandedView
+                .animate()
+                .scaleX(EXPANDED_VIEW_DRAG_SCALE)
+                .scaleY(EXPANDED_VIEW_DRAG_SCALE)
+                .setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION)
+                .setInterpolator(Interpolators.EMPHASIZED)
+                .withStartAction(() -> expandedView.setAnimating(true))
+                .withEndAction(() -> expandedView.setAnimating(false))
+                .start();
+    }
+
+    /**
      * Cancel current animations
      */
     public void cancelAnimations() {
         PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
         mExpandedViewAlphaAnimator.cancel();
+        BubbleBarExpandedView bbev = getExpandedView();
+        if (bbev != null) {
+            bbev.animate().cancel();
+        }
     }
 
     private @Nullable BubbleBarExpandedView getExpandedView() {
@@ -231,21 +407,34 @@
             return;
         }
 
-        boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
-        final int padding = mPositioner.getBubbleBarExpandedViewPadding();
-        final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
-        final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
+        final Size size = getExpandedViewSize();
+        Point position = getExpandedViewRestPosition(size);
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams();
-        lp.width = width;
-        lp.height = height;
+        lp.width = size.getWidth();
+        lp.height = size.getHeight();
         bbev.setLayoutParams(lp);
-        if (mLayerView.isOnLeft()) {
-            bbev.setX(mPositioner.getInsets().left + padding);
-        } else {
-            bbev.setX(mPositioner.getAvailableRect().width() - width - padding);
-        }
-        bbev.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
+        bbev.setX(position.x);
+        bbev.setY(position.y);
         bbev.updateLocation();
         bbev.maybeShowOverflow();
     }
+
+    private Point getExpandedViewRestPosition(Size size) {
+        final int padding = mPositioner.getBubbleBarExpandedViewPadding();
+        Point point = new Point();
+        if (mLayerView.isOnLeft()) {
+            point.x = mPositioner.getInsets().left + padding;
+        } else {
+            point.x = mPositioner.getAvailableRect().width() - size.getWidth() - padding;
+        }
+        point.y = mPositioner.getExpandedViewBottomForBubbleBar() - size.getHeight();
+        return point;
+    }
+
+    private Size getExpandedViewSize() {
+        boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
+        final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
+        final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
+        return new Size(width, height);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
index 4ea18f7..5e634a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
@@ -16,70 +16,70 @@
 
 package com.android.wm.shell.bubbles.bar
 
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.graphics.PointF
-import android.graphics.Rect
+import android.annotation.SuppressLint
 import android.view.MotionEvent
 import android.view.View
-import com.android.wm.shell.animation.Interpolators
 import com.android.wm.shell.common.bubbles.DismissView
 import com.android.wm.shell.common.bubbles.RelativeTouchListener
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject
 
 /** Controller for handling drag interactions with [BubbleBarExpandedView] */
+@SuppressLint("ClickableViewAccessibility")
 class BubbleBarExpandedViewDragController(
     private val expandedView: BubbleBarExpandedView,
     private val dismissView: DismissView,
+    private val animationHelper: BubbleBarAnimationHelper,
     private val onDismissed: () -> Unit
 ) {
 
+    var isStuckToDismiss: Boolean = false
+        private set
+
+    private var expandedViewInitialTranslationX = 0f
+    private var expandedViewInitialTranslationY = 0f
+    private val magnetizedExpandedView: MagnetizedObject<BubbleBarExpandedView> =
+        MagnetizedObject.magnetizeView(expandedView)
+    private val magnetizedDismissTarget: MagnetizedObject.MagneticTarget
+
     init {
-        expandedView.handleView.setOnTouchListener(HandleDragListener())
-    }
-
-    private fun finishDrag(x: Float, y: Float, viewInitialX: Float, viewInitialY: Float) {
-        val dismissCircleBounds = Rect().apply { dismissView.circle.getBoundsOnScreen(this) }
-        if (dismissCircleBounds.contains(x.toInt(), y.toInt())) {
-            onDismissed()
-        } else {
-            resetExpandedViewPosition(viewInitialX, viewInitialY)
-        }
-        dismissView.hide()
-    }
-
-    private fun resetExpandedViewPosition(initialX: Float, initialY: Float) {
-        val listener =
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationStart(animation: Animator) {
-                    expandedView.isAnimating = true
-                }
-
-                override fun onAnimationEnd(animation: Animator) {
-                    expandedView.isAnimating = false
-                }
+        magnetizedExpandedView.magnetListener = MagnetListener()
+        magnetizedExpandedView.animateStuckToTarget =
+            {
+                    target: MagnetizedObject.MagneticTarget,
+                    _: Float,
+                    _: Float,
+                    _: Boolean,
+                    after: (() -> Unit)? ->
+                animationHelper.animateIntoTarget(target, after)
             }
-        expandedView
-            .animate()
-            .translationX(initialX)
-            .translationY(initialY)
-            .setDuration(RESET_POSITION_ANIM_DURATION)
-            .setInterpolator(Interpolators.EMPHASIZED_DECELERATE)
-            .setListener(listener)
-            .start()
+
+        magnetizedDismissTarget =
+            MagnetizedObject.MagneticTarget(dismissView.circle, dismissView.circle.width)
+        magnetizedExpandedView.addTarget(magnetizedDismissTarget)
+
+        val dragMotionEventHandler = HandleDragListener()
+
+        expandedView.handleView.setOnTouchListener { view, event ->
+            if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+                expandedViewInitialTranslationX = expandedView.translationX
+                expandedViewInitialTranslationY = expandedView.translationY
+            }
+            val magnetConsumed = magnetizedExpandedView.maybeConsumeMotionEvent(event)
+            // Move events can be consumed by the magnetized object
+            if (event.actionMasked == MotionEvent.ACTION_MOVE && magnetConsumed) {
+                return@setOnTouchListener true
+            }
+            return@setOnTouchListener dragMotionEventHandler.onTouch(view, event) || magnetConsumed
+        }
     }
 
     private inner class HandleDragListener : RelativeTouchListener() {
 
-        private val expandedViewRestPosition = PointF()
+        private var isMoving = false
 
         override fun onDown(v: View, ev: MotionEvent): Boolean {
             // While animating, don't allow new touch events
-            if (expandedView.isAnimating) {
-                return false
-            }
-            expandedViewRestPosition.x = expandedView.translationX
-            expandedViewRestPosition.y = expandedView.translationY
-            return true
+            return !expandedView.isAnimating
         }
 
         override fun onMove(
@@ -90,8 +90,12 @@
             dx: Float,
             dy: Float
         ) {
-            expandedView.translationX = expandedViewRestPosition.x + dx
-            expandedView.translationY = expandedViewRestPosition.y + dy
+            if (!isMoving) {
+                isMoving = true
+                animationHelper.animateStartDrag()
+            }
+            expandedView.translationX = expandedViewInitialTranslationX + dx
+            expandedView.translationY = expandedViewInitialTranslationY + dy
             dismissView.show()
         }
 
@@ -105,16 +109,41 @@
             velX: Float,
             velY: Float
         ) {
-            finishDrag(ev.rawX, ev.rawY, expandedViewRestPosition.x, expandedViewRestPosition.y)
+            finishDrag()
         }
 
         override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) {
-            resetExpandedViewPosition(expandedViewRestPosition.x, expandedViewRestPosition.y)
-            dismissView.hide()
+            finishDrag()
+        }
+
+        private fun finishDrag() {
+            if (!isStuckToDismiss) {
+                animationHelper.animateToRestPosition()
+                dismissView.hide()
+            }
+            isMoving = false
         }
     }
 
-    companion object {
-        const val RESET_POSITION_ANIM_DURATION = 300L
+    private inner class MagnetListener : MagnetizedObject.MagnetListener {
+        override fun onStuckToTarget(target: MagnetizedObject.MagneticTarget) {
+            isStuckToDismiss = true
+        }
+
+        override fun onUnstuckFromTarget(
+            target: MagnetizedObject.MagneticTarget,
+            velX: Float,
+            velY: Float,
+            wasFlungOut: Boolean
+        ) {
+            isStuckToDismiss = false
+            animationHelper.animateUnstuckFromDismissView()
+        }
+
+        override fun onReleasedInTarget(target: MagnetizedObject.MagneticTarget) {
+            onDismissed()
+            dismissView.hide()
+        }
     }
 }
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index bdb0e20..1211451 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -18,6 +18,7 @@
 
 import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
 import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -36,7 +37,6 @@
 import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
-import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.bubbles.DeviceConfig;
 import com.android.wm.shell.bubbles.DismissViewUtils;
 import com.android.wm.shell.common.bubbles.DismissView;
@@ -206,10 +206,13 @@
                 }
             });
 
-            mDragController = new BubbleBarExpandedViewDragController(mExpandedView, mDismissView,
+            mDragController = new BubbleBarExpandedViewDragController(
+                    mExpandedView,
+                    mDismissView,
+                    mAnimationHelper,
                     () -> {
                         mBubbleController.dismissBubble(mExpandedBubble.getKey(),
-                                Bubbles.DISMISS_USER_GESTURE);
+                                DISMISS_USER_GESTURE);
                         return Unit.INSTANCE;
                     });
 
@@ -241,7 +244,11 @@
         mIsExpanded = false;
         final BubbleBarExpandedView viewToRemove = mExpandedView;
         mEducationViewController.hideEducation(/* animated = */ true);
-        mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
+        if (mDragController != null && mDragController.isStuckToDismiss()) {
+            mAnimationHelper.animateDismiss(() -> removeView(viewToRemove));
+        } else {
+            mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
+        }
         mBubbleController.getSysuiProxy().onStackExpandChanged(false);
         mExpandedView = null;
         mDragController = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 3c6bc17..fc97c798 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -363,7 +363,8 @@
             @ShellMainThread ShellExecutor shellExecutor,
             @ShellBackgroundThread Handler backgroundHandler,
             BackAnimationBackground backAnimationBackground,
-            Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry) {
+            Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry,
+            ShellCommandHandler shellCommandHandler) {
         if (BackAnimationController.IS_ENABLED) {
             return shellBackAnimationRegistry.map(
                     (animations) ->
@@ -374,7 +375,8 @@
                                     backgroundHandler,
                                     context,
                                     backAnimationBackground,
-                                    animations));
+                                    animations,
+                                    shellCommandHandler));
         }
         return Optional.empty();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 0448d94..0b8f60e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -96,7 +96,7 @@
 
     @Nullable
     private WindowContainerTransaction getExitPipViaExpandTransaction() {
-        if (mPipTaskToken == null || mPinnedTaskLeash == null) {
+        if (mPipTaskToken == null) {
             return null;
         }
         WindowContainerTransaction wct = new WindowContainerTransaction();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 6200ea5..48a0a46 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PIP;
 
 import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
 
@@ -56,6 +57,8 @@
     private IBinder mAutoEnterButtonNavTransition;
     @Nullable
     private IBinder mExitViaExpandTransition;
+    @Nullable
+    private IBinder mLegacyEnterTransition;
 
     public PipTransition(
             @NonNull ShellInit shellInit,
@@ -98,6 +101,9 @@
         if (isAutoEnterInButtonNavigation(request)) {
             mAutoEnterButtonNavTransition = transition;
             return getEnterPipTransaction(transition, request);
+        } else if (isLegacyEnter(request)) {
+            mLegacyEnterTransition = transition;
+            return getEnterPipTransaction(transition, request);
         }
         return null;
     }
@@ -108,6 +114,9 @@
         if (isAutoEnterInButtonNavigation(request)) {
             outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */);
             mAutoEnterButtonNavTransition = transition;
+        } else if (isLegacyEnter(request)) {
+            outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */);
+            mLegacyEnterTransition = transition;
         }
     }
 
@@ -153,6 +162,10 @@
                 && pipTask.pictureInPictureParams.isAutoEnterEnabled();
     }
 
+    private boolean isLegacyEnter(@NonNull TransitionRequestInfo requestInfo) {
+        return requestInfo.getType() == TRANSIT_PIP;
+    }
+
     @Override
     public boolean startAnimation(@NonNull IBinder transition,
             @NonNull TransitionInfo info,
@@ -161,29 +174,65 @@
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         if (transition == mAutoEnterButtonNavTransition) {
             mAutoEnterButtonNavTransition = null;
-            TransitionInfo.Change pipChange = getPipChange(info);
-            if (pipChange == null) {
-                return false;
-            }
-            mPipTaskToken = pipChange.getContainer();
-
-            // cache the PiP task token and leash
-            mPipScheduler.setPipTaskToken(mPipTaskToken);
-            mPipScheduler.setPinnedTaskLeash(pipChange.getLeash());
-
-            startTransaction.apply();
-            finishCallback.onTransitionFinished(null);
-            return true;
+            return startAutoEnterButtonNavAnimation(info, startTransaction, finishTransaction,
+                    finishCallback);
+        } else if (transition == mLegacyEnterTransition) {
+            mLegacyEnterTransition = null;
+            return startLegacyEnterAnimation(info, startTransaction, finishTransaction,
+                    finishCallback);
         } else if (transition == mExitViaExpandTransition) {
             mExitViaExpandTransition = null;
-            startTransaction.apply();
-            finishCallback.onTransitionFinished(null);
-            onExitPip();
-            return true;
+            return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
         }
         return false;
     }
 
+    private boolean startAutoEnterButtonNavAnimation(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        TransitionInfo.Change pipChange = getPipChange(info);
+        if (pipChange == null) {
+            return false;
+        }
+        mPipTaskToken = pipChange.getContainer();
+
+        // cache the PiP task token and leash
+        mPipScheduler.setPipTaskToken(mPipTaskToken);
+
+        startTransaction.apply();
+        finishCallback.onTransitionFinished(null);
+        return true;
+    }
+
+    private boolean startLegacyEnterAnimation(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        TransitionInfo.Change pipChange = getPipChange(info);
+        if (pipChange == null) {
+            return false;
+        }
+        mPipTaskToken = pipChange.getContainer();
+
+        // cache the PiP task token and leash
+        mPipScheduler.setPipTaskToken(mPipTaskToken);
+
+        startTransaction.apply();
+        finishCallback.onTransitionFinished(null);
+        return true;
+    }
+
+    private boolean startExpandAnimation(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        startTransaction.apply();
+        finishCallback.onTransitionFinished(null);
+        onExitPip();
+        return true;
+    }
+
     @Nullable
     private TransitionInfo.Change getPipChange(TransitionInfo info) {
         for (TransitionInfo.Change change : info.getChanges()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index ae21c4b..f58aeac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -277,11 +277,6 @@
         params.token = appToken;
         params.packageName = activityInfo.packageName;
         params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
-
-        if (!context.getResources().getCompatibilityInfo().supportsScreen()) {
-            params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
-        }
-
         params.setTitle("Splash Screen " + title);
         return params;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index 473deba..af31f5f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.transition;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 
 import static com.android.wm.shell.transition.Transitions.TransitionObserver;
@@ -35,7 +36,8 @@
 
 /**
  * The {@link TransitionObserver} that observes for transitions involving the home
- * activity. It reports transitions to the caller via {@link IHomeTransitionListener}.
+ * activity on the {@link android.view.Display#DEFAULT_DISPLAY} only.
+ * It reports transitions to the caller via {@link IHomeTransitionListener}.
  */
 public class HomeTransitionObserver implements TransitionObserver,
         RemoteCallable<HomeTransitionObserver> {
@@ -58,6 +60,7 @@
         for (TransitionInfo.Change change : info.getChanges()) {
             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
             if (taskInfo == null
+                    || taskInfo.displayId != DEFAULT_DISPLAY
                     || taskInfo.taskId == -1
                     || !taskInfo.isRunning) {
                 continue;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
index 18716c6..72fba3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
@@ -20,12 +20,13 @@
 import android.window.TransitionFilter;
 
 /**
- *  Listener interface that Launcher attaches to SystemUI to get home activity transition callbacks.
+ * Listener interface that Launcher attaches to SystemUI to get home activity transition callbacks
+ * on the default display.
  */
-interface IHomeTransitionListener {
+oneway interface IHomeTransitionListener {
 
     /**
-     * Called when a transition changes the visibility of the home activity.
+     * Called when a transition changes the visibility of the home activity on the default display.
      */
     void onHomeVisibilityChanged(in boolean isVisible);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index af69b52..b0d8b47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -757,6 +757,10 @@
             }
             if (!change.hasFlags(FLAG_IS_OCCLUDED)) {
                 allOccluded = false;
+            } else if (change.hasAllFlags(TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION)) {
+                // Remove the change because it should be invisible in the animation.
+                info.getChanges().remove(i);
+                continue;
             }
             // The change has already animated by back gesture, don't need to play transition
             // animation on it.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index c12ac8b..6e7d11d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
+import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
@@ -34,6 +35,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
 /**
@@ -84,6 +86,69 @@
         mDragPositioningCallback = dragPositioningCallback;
     }
 
+    @Override
+    Rect calculateValidDragArea() {
+        final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(),
+                R.dimen.caption_left_buttons_width);
+
+        // On a smaller screen, don't require as much empty space on screen, as offscreen
+        // drags will be restricted too much.
+        final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.taskId)
+                .getResources().getConfiguration().smallestScreenWidthDp >= 600
+                ? R.dimen.freeform_required_visible_empty_space_in_header :
+                R.dimen.small_screen_required_visible_empty_space_in_header;
+        final int requiredEmptySpace = loadDimensionPixelSize(mContext.getResources(),
+                requiredEmptySpaceId);
+
+        final int rightButtonsWidth = loadDimensionPixelSize(mContext.getResources(),
+                R.dimen.caption_right_buttons_width);
+        final int taskWidth = mTaskInfo.configuration.windowConfiguration.getBounds().width();
+        final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
+        final int displayWidth = layout.width();
+        final Rect stableBounds = new Rect();
+        layout.getStableBounds(stableBounds);
+        return new Rect(
+                determineMinX(leftButtonsWidth, rightButtonsWidth, requiredEmptySpace,
+                        taskWidth),
+                stableBounds.top,
+                determineMaxX(leftButtonsWidth, rightButtonsWidth, requiredEmptySpace, taskWidth,
+                        displayWidth),
+                determineMaxY(requiredEmptySpace, stableBounds));
+    }
+
+
+    /**
+     * Determine the lowest x coordinate of a freeform task. Used for restricting drag inputs.
+     */
+    private int determineMinX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace,
+            int taskWidth) {
+        // Do not let apps with < 48dp empty header space go off the left edge at all.
+        if (leftButtonsWidth + rightButtonsWidth + requiredEmptySpace > taskWidth) {
+            return 0;
+        }
+        return -taskWidth + requiredEmptySpace + rightButtonsWidth;
+    }
+
+    /**
+     * Determine the highest x coordinate of a freeform task. Used for restricting drag inputs.
+     */
+    private int determineMaxX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace,
+            int taskWidth, int displayWidth) {
+        // Do not let apps with < 48dp empty header space go off the right edge at all.
+        if (leftButtonsWidth + rightButtonsWidth + requiredEmptySpace > taskWidth) {
+            return displayWidth - taskWidth;
+        }
+        return displayWidth - requiredEmptySpace - leftButtonsWidth;
+    }
+
+    /**
+     * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs.
+     */
+    private int determineMaxY(int requiredEmptySpace, Rect stableBounds) {
+        return stableBounds.bottom - requiredEmptySpace;
+    }
+
+
     void setDragDetector(DragDetector dragDetector) {
         mDragDetector = dragDetector;
         mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
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 6ec91e0..3b6be8a 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
@@ -443,6 +443,66 @@
     }
 
     /**
+     * Determine valid drag area for this task based on elements in the app chip.
+     */
+    @Override
+    Rect calculateValidDragArea() {
+        final int appTextWidth = ((DesktopModeAppControlsWindowDecorationViewHolder)
+                mWindowDecorViewHolder).getAppNameTextWidth();
+        final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(),
+                R.dimen.desktop_mode_app_details_width_minus_text) + appTextWidth;
+        final int requiredEmptySpace = loadDimensionPixelSize(mContext.getResources(),
+                R.dimen.freeform_required_visible_empty_space_in_header);
+        final int rightButtonsWidth = loadDimensionPixelSize(mContext.getResources(),
+                R.dimen.desktop_mode_right_edge_buttons_width);
+        final int taskWidth = mTaskInfo.configuration.windowConfiguration.getBounds().width();
+        final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
+        final int displayWidth = layout.width();
+        final Rect stableBounds = new Rect();
+        layout.getStableBounds(stableBounds);
+        return new Rect(
+                determineMinX(leftButtonsWidth, rightButtonsWidth, requiredEmptySpace,
+                        taskWidth),
+                stableBounds.top,
+                determineMaxX(leftButtonsWidth, rightButtonsWidth, requiredEmptySpace,
+                        taskWidth, displayWidth),
+                determineMaxY(requiredEmptySpace, stableBounds));
+    }
+
+
+    /**
+     * Determine the lowest x coordinate of a freeform task. Used for restricting drag inputs.
+     */
+    private int determineMinX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace,
+            int taskWidth) {
+        // Do not let apps with < 48dp empty header space go off the left edge at all.
+        if (leftButtonsWidth + rightButtonsWidth + requiredEmptySpace > taskWidth) {
+            return 0;
+        }
+        return -taskWidth + requiredEmptySpace + rightButtonsWidth;
+    }
+
+    /**
+     * Determine the highest x coordinate of a freeform task. Used for restricting drag inputs.
+     */
+    private int determineMaxX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace,
+            int taskWidth, int displayWidth) {
+        // Do not let apps with < 48dp empty header space go off the right edge at all.
+        if (leftButtonsWidth + rightButtonsWidth + requiredEmptySpace > taskWidth) {
+            return displayWidth - taskWidth;
+        }
+        return displayWidth - requiredEmptySpace - leftButtonsWidth;
+    }
+
+    /**
+     * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs.
+     */
+    private int determineMaxY(int requiredEmptySpace, Rect stableBounds) {
+        return stableBounds.bottom - requiredEmptySpace;
+    }
+
+
+    /**
      * Create and display maximize menu window
      */
     void createMaximizeMenu() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index cb0a6c7..677c7f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -162,18 +162,29 @@
 
     /**
      * Updates repositionTaskBounds to the final bounds of the task after the drag is finished. If
-     * the bounds are outside of the stable bounds, they are shifted to place task at the top of the
-     * stable bounds.
+     * the bounds are outside of the valid drag area, the task is shifted back onto the edge of the
+     * valid drag area.
      */
-    static void onDragEnd(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, Rect stableBounds,
-            PointF repositionStartPoint, float x, float y)  {
+    static void onDragEnd(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
+            PointF repositionStartPoint, float x, float y, Rect validDragArea) {
         updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint,
                 x, y);
+        snapTaskBoundsIfNecessary(repositionTaskBounds, validDragArea);
+    }
 
-        // If task is outside of stable bounds (in the status bar area), shift the task down.
-        if (stableBounds.top > repositionTaskBounds.top) {
-            final int yShift =  stableBounds.top - repositionTaskBounds.top;
-            repositionTaskBounds.offset(0, yShift);
+    private static void snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) {
+        // If we were never supplied a valid drag area, do not restrict movement.
+        // Otherwise, we restrict deltas to keep task position inside the Rect.
+        if (validDragArea.width() == 0) return;
+        if (repositionTaskBounds.left < validDragArea.left) {
+            repositionTaskBounds.offset(validDragArea.left - repositionTaskBounds.left, 0);
+        } else if (repositionTaskBounds.left > validDragArea.right) {
+            repositionTaskBounds.offset(validDragArea.right - repositionTaskBounds.left, 0);
+        }
+        if (repositionTaskBounds.top < validDragArea.top) {
+            repositionTaskBounds.offset(0, validDragArea.top - repositionTaskBounds.top);
+        } else if (repositionTaskBounds.top > validDragArea.bottom) {
+            repositionTaskBounds.offset(0, validDragArea.bottom - repositionTaskBounds.top);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 3a1ea0e..5d006fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -136,7 +136,8 @@
                 y)) {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
             DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
-                    mTaskBoundsAtDragStart, mStableBounds, mRepositionStartPoint, x, y);
+                    mTaskBoundsAtDragStart, mRepositionStartPoint, x, y,
+                    mWindowDecoration.calculateValidDragArea());
             wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
             mTaskOrganizer.applyTransaction(wct);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 4b55a0c..4363558 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -152,7 +152,8 @@
                 mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint,
                 y)) {
             DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
-                    mTaskBoundsAtDragStart, mStableBounds, mRepositionStartPoint, x, y);
+                    mTaskBoundsAtDragStart, mRepositionStartPoint, x, y,
+                    mDesktopWindowDecoration.calculateValidDragArea());
             DragPositioningCallbackUtility.applyTaskBoundsChange(new WindowContainerTransaction(),
                     mDesktopWindowDecoration, mRepositionTaskBounds, mTaskOrganizer);
         }
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 634b755..ee0e31e 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
@@ -21,6 +21,7 @@
 import static android.view.WindowInsets.Type.statusBars;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.Context;
@@ -178,6 +179,13 @@
      */
     abstract void relayout(RunningTaskInfo taskInfo);
 
+    /**
+     * Used by the {@link DragPositioningCallback} associated with the implementing class to
+     * enforce drags ending in a valid position. A null result means no restriction.
+     */
+    @Nullable
+    abstract Rect calculateValidDragArea();
+
     void relayout(RelayoutParams params, SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView,
             RelayoutResult<T> outResult) {
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 589a813..144373f 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
@@ -42,6 +42,8 @@
     private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window)
     private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
     private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
+    val appNameTextWidth: Int
+        get() = appNameTextView.width
 
     init {
         captionView.setOnTouchListener(onCaptionTouchListener)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 771876f..9ded6ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -63,6 +63,7 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.sysui.ShellSharedConstants;
@@ -110,6 +111,8 @@
 
     @Mock
     private InputManager mInputManager;
+    @Mock
+    private ShellCommandHandler mShellCommandHandler;
 
     private BackAnimationController mController;
     private TestableContentResolver mContentResolver;
@@ -145,7 +148,8 @@
                         mContext,
                         mContentResolver,
                         mAnimationBackground,
-                        mShellBackAnimationRegistry);
+                        mShellBackAnimationRegistry,
+                        mShellCommandHandler);
         mShellInit.init();
         mShellExecutor.flushAll();
     }
@@ -298,7 +302,8 @@
                         mContext,
                         mContentResolver,
                         mAnimationBackground,
-                        mShellBackAnimationRegistry);
+                        mShellBackAnimationRegistry,
+                        mShellCommandHandler);
         shellInit.init();
         registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
index 874ef80..91503b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
@@ -53,6 +53,7 @@
                 /* progress = */ progress,
                 /* velocityX = */ 0,
                 /* velocityY = */ 0,
+                /* triggerBack = */ false,
                 /* swipeEdge = */ BackEvent.EDGE_LEFT,
                 /* departingAnimationTarget = */ null);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
index c1ff260..60f1d271 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -16,52 +16,51 @@
 
 package com.android.wm.shell.bubbles.animation;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.SuppressLint;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleStackView;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
 
-    private int mDisplayWidth = 500;
-    private int mDisplayHeight = 1000;
-
-    private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
+    private final Semaphore mBubbleRemovedSemaphore = new Semaphore(0);
+    private final Runnable mOnBubbleAnimatedOutAction = mBubbleRemovedSemaphore::release;
     ExpandedAnimationController mExpandedController;
 
     private int mStackOffset;
     private PointF mExpansionPoint;
     private BubblePositioner mPositioner;
-    private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState();
+    private final BubbleStackView.StackViewState mStackViewState =
+            new BubbleStackView.StackViewState();
 
-    @SuppressLint("VisibleForTests")
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -70,15 +69,13 @@
                 getContext().getSystemService(WindowManager.class));
         mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
                 Insets.of(0, 0, 0, 0),
-                new Rect(0, 0, mDisplayWidth, mDisplayHeight));
+                new Rect(0, 0, 500, 1000));
 
         BubbleStackView stackView = mock(BubbleStackView.class);
-        when(stackView.getState()).thenReturn(getStackViewState());
 
         mExpandedController = new ExpandedAnimationController(mPositioner,
                 mOnBubbleAnimatedOutAction,
                 stackView);
-        spyOn(mExpandedController);
 
         addOneMoreThanBubbleLimitBubbles();
         mLayout.setActiveController(mExpandedController);
@@ -86,9 +83,18 @@
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
         mExpansionPoint = new PointF(100, 100);
+
+        getStackViewState();
+        when(stackView.getState()).thenAnswer(i -> getStackViewState());
+        waitForMainThread();
     }
 
-    public BubbleStackView.StackViewState getStackViewState() {
+    @After
+    public void tearDown() {
+        waitForMainThread();
+    }
+
+    private BubbleStackView.StackViewState getStackViewState() {
         mStackViewState.numberOfBubbles = mLayout.getChildCount();
         mStackViewState.selectedIndex = 0;
         mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint);
@@ -96,68 +102,71 @@
     }
 
     @Test
-    @Ignore
-    public void testExpansionAndCollapse() throws InterruptedException {
-        Runnable afterExpand = mock(Runnable.class);
-        mExpandedController.expandFromStack(afterExpand);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
+    public void testExpansionAndCollapse() throws Exception {
+        expand();
         testBubblesInCorrectExpandedPositions();
-        verify(afterExpand).run();
+        waitForMainThread();
 
-        Runnable afterCollapse = mock(Runnable.class);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterCollapse = semaphore::release;
         mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
-        verify(afterExpand).run();
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForAnimation();
+        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y);
     }
 
     @Test
-    @Ignore
-    public void testOnChildAdded() throws InterruptedException {
+    public void testOnChildAdded() throws Exception {
         expand();
+        waitForMainThread();
 
         // Add another new view and wait for its animation.
         final View newView = new FrameLayout(getContext());
         mLayout.addView(newView, 0);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    @Ignore
-    public void testOnChildRemoved() throws InterruptedException {
+    public void testOnChildRemoved() throws Exception {
         expand();
+        waitForMainThread();
 
-        // Remove some views and see if the remaining child views still pass the expansion test.
+        // Remove some views and verify the remaining child views still pass the expansion test.
         mLayout.removeView(mViews.get(0));
         mLayout.removeView(mViews.get(3));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+        // Removing a view will invoke onBubbleAnimatedOutAction. Block until it gets called twice.
+        assertThat(mBubbleRemovedSemaphore.tryAcquire(2, 2, TimeUnit.SECONDS)).isTrue();
+
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    public void testDragBubbleOutDoesntNPE() throws InterruptedException {
+    public void testDragBubbleOutDoesntNPE() {
         mExpandedController.onGestureFinished();
         mExpandedController.dragBubbleOut(mViews.get(0), 1, 1);
     }
 
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
-        mExpandedController.expandFromStack(mock(Runnable.class));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterExpand = semaphore::release;
+
+        mExpandedController.expandFromStack(afterExpand);
+        assertThat(semaphore.tryAcquire(1, TimeUnit.SECONDS)).isTrue();
     }
 
     /** Check that children are in the correct positions for being stacked. */
-    private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
+    private void testStackedAtPosition(float x, float y) {
         // Make sure the rest of the stack moved again, including the first bubble not moving, and
         // is stacked to the right now that we're on the right side of the screen.
         for (int i = 0; i < mLayout.getChildCount(); i++) {
-            assertEquals(x + i * offsetMultiplier * mStackOffset,
-                    mLayout.getChildAt(i).getTranslationX(), 2f);
-            assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
+            assertEquals(x, mLayout.getChildAt(i).getTranslationX(), 2f);
+            assertEquals(y + Math.min(i, 1) * mStackOffset, mLayout.getChildAt(i).getTranslationY(),
+                    2f);
             assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
@@ -175,4 +184,22 @@
                     mLayout.getChildAt(i).getTranslationY(), 2f);
         }
     }
+
+    private void waitForAnimation() throws Exception {
+        final Semaphore semaphore = new Semaphore(0);
+        boolean[] animating = new boolean[]{ true };
+        for (int i = 0; i < 4; i++) {
+            if (animating[0]) {
+                mMainThreadHandler.post(() -> {
+                    if (!mExpandedController.isAnimating()) {
+                        animating[0] = false;
+                        semaphore.release();
+                    }
+                });
+                Thread.sleep(500);
+            }
+        }
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index 48ae296..2ed5add 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -164,11 +164,17 @@
 
         @Override
         public void cancelAllAnimations() {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(super::cancelAllAnimations);
         }
 
         @Override
         public void cancelAnimationsOnView(View view) {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view));
         }
 
@@ -221,6 +227,9 @@
 
             @Override
             protected void startPathAnimation() {
+                if (mLayout.getChildCount() == 0) {
+                    return;
+                }
                 mMainThreadHandler.post(super::startPathAnimation);
             }
         }
@@ -322,4 +331,9 @@
             e.printStackTrace();
         }
     }
+
+    /** Waits for the main thread to finish processing all pending runnables. */
+    public void waitForMainThread() {
+        runOnMainThreadAndBlock(() -> {});
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index 5c0e04a..e60be71 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -181,6 +181,26 @@
     }
 
     @Test
+    fun testDragEndSnapsTaskBoundsWhenOutsideValidDragArea() {
+        val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(STARTING_BOUNDS)
+        val validDragArea = Rect(DISPLAY_BOUNDS.left - 100,
+            STABLE_BOUNDS.top,
+            DISPLAY_BOUNDS.right - 100,
+            DISPLAY_BOUNDS.bottom - 100)
+
+        DragPositioningCallbackUtility.onDragEnd(repositionTaskBounds, STARTING_BOUNDS,
+            startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat(),
+            validDragArea)
+        assertThat(repositionTaskBounds.left).isEqualTo(validDragArea.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(validDragArea.bottom)
+        assertThat(repositionTaskBounds.right)
+            .isEqualTo(validDragArea.left + STARTING_BOUNDS.width())
+        assertThat(repositionTaskBounds.bottom)
+            .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height())
+    }
+
+    @Test
     fun testChangeBounds_toDisallowedBounds_freezesAtLimit() {
         var hasMoved = false
         val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index add78b2..2ce49cf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -103,6 +103,7 @@
             configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
             configuration.windowConfiguration.displayRotation = ROTATION_90
         }
+        `when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA)
         mockWindowDecoration.mDisplay = mockDisplay
         whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
 
@@ -660,6 +661,38 @@
     }
 
     @Test
+    fun testDragResize_drag_taskPositionedInValidDragArea() {
+        taskPositioner.onDragPositioningStart(
+            CTRL_TYPE_UNDEFINED, // drag
+            STARTING_BOUNDS.left.toFloat(),
+            STARTING_BOUNDS.top.toFloat()
+        )
+
+        val newX = VALID_DRAG_AREA.left - 500f
+        val newY = VALID_DRAG_AREA.bottom + 500f
+        taskPositioner.onDragPositioningMove(
+            newX,
+            newY
+        )
+        verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
+
+        taskPositioner.onDragPositioningEnd(
+            newX,
+            newY
+        )
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+                        change.configuration.windowConfiguration.bounds.top ==
+                        VALID_DRAG_AREA.bottom &&
+                        change.configuration.windowConfiguration.bounds.left ==
+                        VALID_DRAG_AREA.left
+            }
+        })
+    }
+
+    @Test
     fun testDragResize_drag_updatesStableBoundsOnRotate() {
         // Test landscape stable bounds
         performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
@@ -761,5 +794,11 @@
             DISPLAY_BOUNDS.bottom,
             DISPLAY_BOUNDS.right - NAVBAR_HEIGHT
         )
+        private val VALID_DRAG_AREA = Rect(
+            DISPLAY_BOUNDS.left - 100,
+            STABLE_BOUNDS_LANDSCAPE.top,
+            DISPLAY_BOUNDS.right - 100,
+            DISPLAY_BOUNDS.bottom - 100
+        )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index a70ebf1..a759b53 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -118,6 +118,7 @@
             configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
             configuration.windowConfiguration.displayRotation = ROTATION_90
         }
+        `when`(mockDesktopWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA)
         mockDesktopWindowDecoration.mDisplay = mockDisplay
         whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
 
@@ -379,6 +380,38 @@
     }
 
     @Test
+    fun testDragResize_drag_taskPositionedInValidDragArea() {
+        taskPositioner.onDragPositioningStart(
+            CTRL_TYPE_UNDEFINED, // drag
+            STARTING_BOUNDS.left.toFloat(),
+            STARTING_BOUNDS.top.toFloat()
+        )
+
+        val newX = VALID_DRAG_AREA.left - 500f
+        val newY = VALID_DRAG_AREA.bottom + 500f
+        taskPositioner.onDragPositioningMove(
+            newX,
+            newY
+        )
+        verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
+
+        taskPositioner.onDragPositioningEnd(
+            newX,
+            newY
+        )
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+                        change.configuration.windowConfiguration.bounds.top ==
+                        VALID_DRAG_AREA.bottom &&
+                        change.configuration.windowConfiguration.bounds.left ==
+                        VALID_DRAG_AREA.left
+            }
+        })
+    }
+
+    @Test
     fun testDragResize_drag_updatesStableBoundsOnRotate() {
         // Test landscape stable bounds
         performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
@@ -470,5 +503,11 @@
             DISPLAY_BOUNDS.bottom,
             DISPLAY_BOUNDS.right - NAVBAR_HEIGHT
         )
+        private val VALID_DRAG_AREA = Rect(
+            DISPLAY_BOUNDS.left - 100,
+            STABLE_BOUNDS_LANDSCAPE.top,
+            DISPLAY_BOUNDS.right - 100,
+            DISPLAY_BOUNDS.bottom - 100
+        )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 8e42f74..fe508e2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -702,6 +702,11 @@
             relayout(taskInfo, false /* applyStartTransactionOnDraw */);
         }
 
+        @Override
+        Rect calculateValidDragArea() {
+            return null;
+        }
+
         void relayout(ActivityManager.RunningTaskInfo taskInfo,
                 boolean applyStartTransactionOnDraw) {
             mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
diff --git a/location/java/android/location/LocationResult.java b/location/java/android/location/LocationResult.java
index 8423000..67f4775 100644
--- a/location/java/android/location/LocationResult.java
+++ b/location/java/android/location/LocationResult.java
@@ -19,8 +19,11 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.location.flags.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Log;
 
 import com.android.internal.util.Preconditions;
 
@@ -37,6 +40,23 @@
  * @hide
  */
 public final class LocationResult implements Parcelable {
+    private static final String TAG = "LocationResult";
+
+    // maximum reasonable accuracy, somewhat arbitrarily chosen. this is a very high upper limit, it
+    // could likely be lower, but we only want to throw out really absurd values.
+    private static final float MAX_ACCURACY_M = 1000000;
+
+    // maximum reasonable speed we expect a device to travel at is currently mach 1 (top speed of
+    // current fastest private jet). Higher speed than the value is considered as a malfunction
+    // than a correct reading.
+    private static final float MAX_SPEED_MPS = 343;
+
+    /** Exception representing an invalid location within a {@link LocationResult}. */
+    public static class BadLocationException extends Exception {
+        public BadLocationException(String message) {
+            super(message);
+        }
+    }
 
     /**
      * Creates a new LocationResult from the given locations, making a copy of each location.
@@ -101,18 +121,60 @@
      *
      * @hide
      */
-    public @NonNull LocationResult validate() {
+    public @NonNull LocationResult validate() throws BadLocationException {
         long prevElapsedRealtimeNs = 0;
         final int size = mLocations.size();
         for (int i = 0; i < size; ++i) {
             Location location = mLocations.get(i);
-            if (!location.isComplete()) {
-                throw new IllegalArgumentException(
-                        "incomplete location at index " + i + ": " + mLocations);
-            }
-            if (location.getElapsedRealtimeNanos() < prevElapsedRealtimeNs) {
-                throw new IllegalArgumentException(
-                        "incorrectly ordered location at index " + i + ": " + mLocations);
+            if (Flags.locationValidation()) {
+                if (location.getLatitude() < -90.0
+                        || location.getLatitude() > 90.0
+                        || location.getLongitude() < -180.0
+                        || location.getLongitude() > 180.0
+                        || Double.isNaN(location.getLatitude())
+                        || Double.isNaN(location.getLongitude())) {
+                    throw new BadLocationException("location must have valid lat/lng");
+                }
+                if (!location.hasAccuracy()) {
+                    throw new BadLocationException("location must have accuracy");
+                }
+                if (location.getAccuracy() < 0 || location.getAccuracy() > MAX_ACCURACY_M) {
+                    throw new BadLocationException("location must have reasonable accuracy");
+                }
+                if (location.getTime() < 0) {
+                    throw new BadLocationException("location must have valid time");
+                }
+                if (prevElapsedRealtimeNs > location.getElapsedRealtimeNanos()) {
+                    throw new BadLocationException(
+                            "location must have valid monotonically increasing realtime");
+                }
+                if (location.getElapsedRealtimeNanos()
+                        > SystemClock.elapsedRealtimeNanos()) {
+                    throw new BadLocationException("location must not have realtime in the future");
+                }
+                if (!location.isMock()) {
+                    if (location.getProvider() == null) {
+                        throw new BadLocationException("location must have valid provider");
+                    }
+                    if (location.getLatitude() == 0 && location.getLongitude() == 0) {
+                        throw new BadLocationException("location must not be at 0,0");
+                    }
+                }
+
+                if (location.hasSpeed() && (location.getSpeed() < 0
+                        || location.getSpeed() > MAX_SPEED_MPS)) {
+                    Log.w(TAG, "removed bad location speed: " + location.getSpeed());
+                    location.removeSpeed();
+                }
+            } else {
+                if (!location.isComplete()) {
+                    throw new IllegalArgumentException(
+                            "incomplete location at index " + i + ": " + mLocations);
+                }
+                if (location.getElapsedRealtimeNanos() < prevElapsedRealtimeNs) {
+                    throw new IllegalArgumentException(
+                            "incorrectly ordered location at index " + i + ": " + mLocations);
+                }
             }
             prevElapsedRealtimeNs = location.getElapsedRealtimeNanos();
         }
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig
index b6055e8..a8464d3 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/gnss.aconfig
@@ -20,3 +20,17 @@
     description: "Flag for GnssMeasurementRequest WorkSource API"
     bug: "295235160"
 }
+
+flag {
+    name: "release_supl_connection_on_timeout"
+    namespace: "location"
+    description: "Flag for releasing SUPL connection on timeout"
+    bug: "315024652"
+}
+
+flag {
+    name: "location_validation"
+    namespace: "location"
+    description: "Flag for location validation"
+    bug: "314328533"
+}
diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java
index 5a72b0b..1a3d7b7 100644
--- a/media/java/android/media/AudioDeviceInfo.java
+++ b/media/java/android/media/AudioDeviceInfo.java
@@ -23,6 +23,8 @@
 import android.annotation.TestApi;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -402,7 +404,9 @@
 
     private final AudioDevicePort mPort;
 
-    AudioDeviceInfo(AudioDevicePort port) {
+    /** @hide */
+    @VisibleForTesting
+    public AudioDeviceInfo(AudioDevicePort port) {
        mPort = port;
     }
 
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 73bc6f9..2de8eef 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -20,6 +20,8 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 
+import com.android.aconfig.annotations.VisibleForTesting;
+
 import java.util.Arrays;
 import java.util.List;
 
@@ -38,6 +40,26 @@
 
 public class AudioDevicePort extends AudioPort {
 
+    /** @hide */
+    // TODO: b/316864909 - Remove this method once there's a way to fake audio device ports further
+    // down the stack.
+    @VisibleForTesting
+    public static AudioDevicePort createForTesting(
+            int type, @NonNull String name, @NonNull String address) {
+        return new AudioDevicePort(
+                new AudioHandle(/* id= */ 0),
+                name,
+                /* samplingRates= */ null,
+                /* channelMasks= */ null,
+                /* channelIndexMasks= */ null,
+                /* formats= */ null,
+                /* gains= */ null,
+                type,
+                address,
+                /* encapsulationModes= */ null,
+                /* encapsulationMetadataTypes= */ null);
+    }
+
     private final int mType;
     private final String mAddress;
     private final int[] mEncapsulationModes;
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3dfd572..a5a69f9 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -22,6 +22,7 @@
 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
 import static android.media.audio.Flags.automaticBtDeviceType;
 import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
@@ -5120,6 +5121,71 @@
     }
 
     /**
+     * Notifies an application with a focus listener of gain or loss of audio focus
+     *
+     * <p>This is similar to {@link #dispatchAudioFocusChange(AudioFocusInfo, int, AudioPolicy)} but
+     * with additional functionality  of fade. The players of the application with  audio focus
+     * change, provided they meet the active {@link FadeManagerConfiguration} requirements, are
+     * faded before dispatching the callback to the application. For example, players of the
+     * application losing audio focus will be faded out, whereas players of the application gaining
+     * audio focus will be faded in, if needed.
+     *
+     * <p>The applicability of fade is decided against the supplied active {@link AudioFocusInfo}.
+     * This list cannot be {@code null}. The list can be empty if no other active
+     * {@link AudioFocusInfo} available at the time of the dispatch.
+     *
+     * <p>The {@link FadeManagerConfiguration} supplied here is prioritized over existing fade
+     * configurations. If none supplied, either the {@link FadeManagerConfiguration} set through
+     * {@link AudioPolicy} or the default will be used to determine the fade properties.
+     *
+     * <p>This method can only be used by owners of an {@link AudioPolicy} configured with
+     * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to true.
+     *
+     * @param afi the recipient of the focus change, that has previously requested audio focus, and
+     *     that was received by the {@code AudioPolicy} through
+     *     {@link AudioPolicy.AudioPolicyFocusListener#onAudioFocusRequest(AudioFocusInfo, int)}
+     * @param focusChange one of focus gain types ({@link #AUDIOFOCUS_GAIN},
+     *     {@link #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or
+     *     {@link #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE})
+     *     or one of the focus loss types ({@link AudioManager#AUDIOFOCUS_LOSS},
+     *     {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT},
+     *     or {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}).
+     *     <br>For the focus gain, the change type should be the same as the app requested
+     * @param ap a valid registered {@link AudioPolicy} configured as a focus policy.
+     * @param otherActiveAfis active {@link AudioFocusInfo} that are granted audio focus at the time
+     *     of dispatch
+     * @param transientFadeMgrConfig {@link FadeManagerConfiguration} that will be used for fading
+     *     players resulting from this dispatch. This is a transient configuration that is only
+     *     valid for this focus change and shall be discarded after processing this request.
+     * @return {@link #AUDIOFOCUS_REQUEST_FAILED} if the focus client didn't have a listener or if
+     *     there was an error sending the request, or {@link #AUDIOFOCUS_REQUEST_GRANTED} if the
+     *     dispatch was successfully sent, or {@link #AUDIOFOCUS_REQUEST_DELAYED} if
+     *     the request was successful but the dispatch of focus change was delayed due to a fade
+     *     operation.
+     * @throws NullPointerException if the {@link AudioFocusInfo} or {@link AudioPolicy} or list of
+     *     other active {@link AudioFocusInfo} are {@code null}.
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int dispatchAudioFocusChangeWithFade(@NonNull AudioFocusInfo afi, int focusChange,
+            @NonNull AudioPolicy ap, @NonNull List<AudioFocusInfo> otherActiveAfis,
+            @Nullable FadeManagerConfiguration transientFadeMgrConfig) {
+        Objects.requireNonNull(afi, "AudioFocusInfo cannot be null");
+        Objects.requireNonNull(ap, "AudioPolicy cannot be null");
+        Objects.requireNonNull(otherActiveAfis, "Other active AudioFocusInfo list cannot be null");
+
+        IAudioService service = getService();
+        try {
+            return service.dispatchFocusChangeWithFade(afi, focusChange, ap.cb(), otherActiveAfis,
+                    transientFadeMgrConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @hide
      * Used internally by telephony package to abandon audio focus, typically after a call or
      * when ringing ends and the call is rejected or not answered.
diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java
index 337d4b0..40b0e3e 100644
--- a/media/java/android/media/FadeManagerConfiguration.java
+++ b/media/java/android/media/FadeManagerConfiguration.java
@@ -16,12 +16,13 @@
 
 package android.media;
 
-import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
@@ -93,11 +94,9 @@
  *      Helps with recreating a new instance from another to simply change/add on top of the
  *      existing ones</li>
  * </ul>
- * TODO(b/304835727): Convert into system API so that it can be set through AudioPolicy
- *
  * @hide
  */
-
+@SystemApi
 @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
 public final class FadeManagerConfiguration implements Parcelable {
 
@@ -523,6 +522,7 @@
      *
      * @param fadeState one of the fade state in {@link FadeStateEnum}
      * @return human-readable string
+     * @hide
      */
     @NonNull
     public static String fadeStateToString(@FadeStateEnum int fadeState) {
@@ -712,7 +712,8 @@
      *
      * <p><b>Notes:</b>
      * <ul>
-     *     <li>When fade state is set to enabled, the builder expects at least one valid usage to be
+     *     <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT} or
+     *     {@link #FADE_STATE_ENABLED_AUTO}, the builder expects at least one valid usage to be
      *     set/added. Failure to do so will result in an exception during {@link #build()}</li>
      *     <li>Every usage added to the fadeable list should have corresponding volume shaper
      *     configs defined. This can be achieved by setting either the duration or volume shaper
@@ -720,8 +721,8 @@
      *     {@link #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration)}</li>
      *     <li> It is recommended to set volume shaper configurations individually for fade out and
      *     fade in</li>
-     *     <li>For any incomplete volume shaper configs a volume shaper configuration will be
-     *     created using either the default fade durations or the ones provided as part of the
+     *     <li>For any incomplete volume shaper configurations, a volume shaper configuration will
+     *     be created using either the default fade durations or the ones provided as part of the
      *     {@link #Builder(long, long)}</li>
      *     <li>Additional volume shaper configs can also configured for a given usage
      *     with additional attributes like content-type in order to achieve finer fade controls.
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index a52f0b0..5c268d4 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -28,6 +28,7 @@
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.BluetoothProfileConnectionInfo;
+import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
 import android.media.IAudioModeDispatcher;
@@ -398,6 +399,14 @@
     int dispatchFocusChange(in AudioFocusInfo afi, in int focusChange,
             in IAudioPolicyCallback pcb);
 
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    int dispatchFocusChangeWithFade(in AudioFocusInfo afi,
+            in int focusChange,
+            in IAudioPolicyCallback pcb,
+            in List<AudioFocusInfo> otherActiveAfis,
+            in FadeManagerConfiguration transientFadeMgrConfig);
+
     oneway void playerHasOpPlayAudio(in int piid, in boolean hasOpPlayAudio);
 
     @EnforcePermission("BLUETOOTH_STACK")
@@ -754,4 +763,16 @@
     oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
 
     PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    int setFadeManagerConfigurationForFocusLoss(in FadeManagerConfiguration fmcForFocusLoss);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    int clearFadeManagerConfigurationForFocusLoss();
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+    FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss();
 }
diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl
index 29bfd1a..e2dddad 100644
--- a/media/java/android/media/IMediaRouter2.aidl
+++ b/media/java/android/media/IMediaRouter2.aidl
@@ -19,6 +19,7 @@
 import android.media.MediaRoute2Info;
 import android.media.RoutingSessionInfo;
 import android.os.Bundle;
+import android.os.UserHandle;
 
 /**
  * @hide
@@ -35,5 +36,6 @@
      * Call MediaRouterService#requestCreateSessionWithRouter2 to pass the result.
      */
     void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession,
-        in MediaRoute2Info route);
+        in MediaRoute2Info route, in UserHandle transferInitiatorUserHandle,
+        in String transferInitiatorPackageName);
 }
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index fa4d1a1..04e99ea 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -64,7 +64,8 @@
 
     void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, long managerRequestId,
             in RoutingSessionInfo oldSession, in MediaRoute2Info route,
-            in @nullable Bundle sessionHints);
+            in @nullable Bundle sessionHints, in UserHandle transferInitiatorUserHandle,
+            in String transferInitiatorPackageName);
     void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route);
     void deselectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route);
     void transferToRouteWithRouter2(IMediaRouter2 router, String sessionId,
@@ -84,13 +85,16 @@
     void stopScan(IMediaRouter2Manager manager);
 
     void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId,
-            in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route);
+            in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route,
+            in UserHandle transferInitiatorUserHandle, in String transferInitiatorPackageName);
     void selectRouteWithManager(IMediaRouter2Manager manager, int requestId,
             String sessionId, in MediaRoute2Info route);
     void deselectRouteWithManager(IMediaRouter2Manager manager, int requestId,
             String sessionId, in MediaRoute2Info route);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)")
     void transferToRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String sessionId, in MediaRoute2Info route);
+            String sessionId, in MediaRoute2Info route,
+            in UserHandle transferInitiatorUserHandle, String transferInitiatorPackageName);
     void setSessionVolumeWithManager(IMediaRouter2Manager manager, int requestId,
             String sessionId, int volume);
     void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId);
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 8ad3587..0eabe66 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -19,6 +19,7 @@
 import static android.media.MediaRouter2Utils.toUniqueId;
 
 import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER;
+import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
 import static com.android.media.flags.Flags.FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES;
 
 import android.annotation.FlaggedApi;
@@ -479,6 +480,37 @@
     public static final String FEATURE_REMOTE_GROUP_PLAYBACK =
             "android.media.route.feature.REMOTE_GROUP_PLAYBACK";
 
+    /** Indicates the route is always suitable for media playback. */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public static final int SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER = 0;
+
+    /**
+     * Indicates that the route is suitable for media playback only after explicit user selection.
+     */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public static final int SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER = 1;
+
+    /** Indicates that the route is never suitable for media playback. */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public static final int SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER = 2;
+
+    /**
+     * Route suitability status.
+     *
+     * <p>Signals whether the route is suitable to play media.
+     *
+     * @hide
+     */
+    @IntDef(
+            value = {
+                SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER,
+                SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER,
+                SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public @interface SuitabilityStatus {}
+
     private final String mId;
     private final CharSequence mName;
     private final List<String> mFeatures;
@@ -500,6 +532,7 @@
     private final String mProviderId;
     private final boolean mIsVisibilityRestricted;
     private final Set<String> mAllowedPackages;
+    @SuitabilityStatus private final int mSuitabilityStatus;
 
     MediaRoute2Info(@NonNull Builder builder) {
         mId = builder.mId;
@@ -521,6 +554,7 @@
         mProviderId = builder.mProviderId;
         mIsVisibilityRestricted = builder.mIsVisibilityRestricted;
         mAllowedPackages = builder.mAllowedPackages;
+        mSuitabilityStatus = builder.mSuitabilityStatus;
     }
 
     MediaRoute2Info(@NonNull Parcel in) {
@@ -544,6 +578,7 @@
         mProviderId = in.readString();
         mIsVisibilityRestricted = in.readBoolean();
         mAllowedPackages = Set.of(in.createString8Array());
+        mSuitabilityStatus = in.readInt();
     }
 
     /**
@@ -778,6 +813,13 @@
                 || mAllowedPackages.contains(packageName);
     }
 
+    /** Returns the route suitability status. */
+    @SuitabilityStatus
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public int getSuitabilityStatus() {
+        return mSuitabilityStatus;
+    }
+
     /**
      * Dumps the current state of the object to the given {@code pw} as a human-readable string.
      *
@@ -809,6 +851,7 @@
         pw.println(indent + "mProviderId=" + mProviderId);
         pw.println(indent + "mIsVisibilityRestricted=" + mIsVisibilityRestricted);
         pw.println(indent + "mAllowedPackages=" + mAllowedPackages);
+        pw.println(indent + "mSuitabilityStatus=" + mSuitabilityStatus);
     }
 
     private void dumpVolume(@NonNull PrintWriter pw, @NonNull String prefix) {
@@ -861,39 +904,74 @@
                 && Objects.equals(mDeduplicationIds, other.mDeduplicationIds)
                 && Objects.equals(mProviderId, other.mProviderId)
                 && (mIsVisibilityRestricted == other.mIsVisibilityRestricted)
-                && Objects.equals(mAllowedPackages, other.mAllowedPackages);
+                && Objects.equals(mAllowedPackages, other.mAllowedPackages)
+                && mSuitabilityStatus == other.mSuitabilityStatus;
     }
 
     @Override
     public int hashCode() {
         // Note: mExtras is not included.
-        return Objects.hash(mId, mName, mFeatures, mType, mIsSystem, mIconUri, mDescription,
-                mConnectionState, mClientPackageName, mPackageName, mVolumeHandling, mVolumeMax,
-                mVolume, mAddress, mDeduplicationIds, mProviderId, mIsVisibilityRestricted,
-                mAllowedPackages);
+        return Objects.hash(
+                mId,
+                mName,
+                mFeatures,
+                mType,
+                mIsSystem,
+                mIconUri,
+                mDescription,
+                mConnectionState,
+                mClientPackageName,
+                mPackageName,
+                mVolumeHandling,
+                mVolumeMax,
+                mVolume,
+                mAddress,
+                mDeduplicationIds,
+                mProviderId,
+                mIsVisibilityRestricted,
+                mAllowedPackages,
+                mSuitabilityStatus);
     }
 
     @Override
     public String toString() {
         // Note: mExtras is not printed here.
-        StringBuilder result = new StringBuilder()
-                .append("MediaRoute2Info{ ")
-                .append("id=").append(getId())
-                .append(", name=").append(getName())
-                .append(", features=").append(getFeatures())
-                .append(", iconUri=").append(getIconUri())
-                .append(", description=").append(getDescription())
-                .append(", connectionState=").append(getConnectionState())
-                .append(", clientPackageName=").append(getClientPackageName())
-                .append(", volumeHandling=").append(getVolumeHandling())
-                .append(", volumeMax=").append(getVolumeMax())
-                .append(", volume=").append(getVolume())
-                .append(", address=").append(getAddress())
-                .append(", deduplicationIds=").append(String.join(",", getDeduplicationIds()))
-                .append(", providerId=").append(getProviderId())
-                .append(", isVisibilityRestricted=").append(mIsVisibilityRestricted)
-                .append(", allowedPackages=").append(String.join(",", mAllowedPackages))
-                .append(" }");
+        StringBuilder result =
+                new StringBuilder()
+                        .append("MediaRoute2Info{ ")
+                        .append("id=")
+                        .append(getId())
+                        .append(", name=")
+                        .append(getName())
+                        .append(", features=")
+                        .append(getFeatures())
+                        .append(", iconUri=")
+                        .append(getIconUri())
+                        .append(", description=")
+                        .append(getDescription())
+                        .append(", connectionState=")
+                        .append(getConnectionState())
+                        .append(", clientPackageName=")
+                        .append(getClientPackageName())
+                        .append(", volumeHandling=")
+                        .append(getVolumeHandling())
+                        .append(", volumeMax=")
+                        .append(getVolumeMax())
+                        .append(", volume=")
+                        .append(getVolume())
+                        .append(", address=")
+                        .append(getAddress())
+                        .append(", deduplicationIds=")
+                        .append(String.join(",", getDeduplicationIds()))
+                        .append(", providerId=")
+                        .append(getProviderId())
+                        .append(", isVisibilityRestricted=")
+                        .append(mIsVisibilityRestricted)
+                        .append(", allowedPackages=")
+                        .append(String.join(",", mAllowedPackages))
+                        .append(", suitabilityStatus=")
+                        .append(mSuitabilityStatus)
+                        .append(" }");
         return result.toString();
     }
 
@@ -923,6 +1001,7 @@
         dest.writeString(mProviderId);
         dest.writeBoolean(mIsVisibilityRestricted);
         dest.writeString8Array(mAllowedPackages.toArray(new String[0]));
+        dest.writeInt(mSuitabilityStatus);
     }
 
     private static String getDeviceTypeString(@Type int deviceType) {
@@ -1005,6 +1084,7 @@
         private String mProviderId;
         private boolean mIsVisibilityRestricted;
         private Set<String> mAllowedPackages;
+        @SuitabilityStatus private int mSuitabilityStatus;
 
         /**
          * Constructor for builder to create {@link MediaRoute2Info}.
@@ -1028,6 +1108,7 @@
             mFeatures = new ArrayList<>();
             mDeduplicationIds = Set.of();
             mAllowedPackages = Set.of();
+            mSuitabilityStatus = SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER;
         }
 
         /**
@@ -1075,6 +1156,7 @@
             mProviderId = routeInfo.mProviderId;
             mIsVisibilityRestricted = routeInfo.mIsVisibilityRestricted;
             mAllowedPackages = routeInfo.mAllowedPackages;
+            mSuitabilityStatus = routeInfo.mSuitabilityStatus;
         }
 
         /**
@@ -1318,6 +1400,23 @@
         }
 
         /**
+         * Sets route suitability status.
+         *
+         * <p>The default value is {@link
+         * MediaRoute2Info#SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER}.
+         *
+         * <p> Apps are not supposed to set {@link
+         * MediaRoute2Info#SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER}. Publishing a non-system
+         * route with such status throws {@link SecurityException}.
+         */
+        @NonNull
+        @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+        public Builder setSuitabilityStatus(@SuitabilityStatus int suitabilityStatus) {
+            mSuitabilityStatus = suitabilityStatus;
+            return this;
+        }
+
+        /**
          * Builds the {@link MediaRoute2Info media route info}.
          *
          * @throws IllegalArgumentException if no features are added.
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index ba26df9..5e23551 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
 import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
 import static com.android.media.flags.Flags.FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2;
 
@@ -699,15 +700,48 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
+    @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
     public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
-        mImpl.transfer(controller.getRoutingSessionInfo(), route);
+        mImpl.transfer(
+                controller.getRoutingSessionInfo(),
+                route,
+                android.os.Process.myUserHandle(),
+                mContext.getPackageName());
+    }
+
+    /**
+     * Transfers the media of a routing controller to the given route.
+     *
+     * <p>This will be no-op for non-system media routers.
+     *
+     * @param controller a routing controller controlling media routing.
+     * @param route the route you want to transfer the media to.
+     * @param transferInitiatorUserHandle the user handle of the app that initiated the transfer
+     *     request.
+     * @param transferInitiatorPackageName the package name of the app that initiated the transfer.
+     *     This value is used with the user handle to populate {@link
+     *     RoutingController#wasTransferRequestedBySelf()}.
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public void transfer(
+            @NonNull RoutingController controller,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
+        mImpl.transfer(
+                controller.getRoutingSessionInfo(),
+                route,
+                transferInitiatorUserHandle,
+                transferInitiatorPackageName);
     }
 
     void requestCreateController(
             @NonNull RoutingController controller,
             @NonNull MediaRoute2Info route,
-            long managerRequestId) {
+            long managerRequestId,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
 
         final int requestId = mNextRequestId.getAndIncrement();
 
@@ -736,7 +770,9 @@
                         managerRequestId,
                         controller.getRoutingSessionInfo(),
                         route,
-                        controllerHints);
+                        controllerHints,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             } catch (RemoteException ex) {
                 Log.e(TAG, "createControllerForTransfer: "
                                 + "Failed to request for creating a controller.", ex);
@@ -1053,7 +1089,11 @@
     }
 
     void onRequestCreateControllerByManagerOnHandler(
-            RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) {
+            RoutingSessionInfo oldSession,
+            MediaRoute2Info route,
+            long managerRequestId,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         Log.i(
                 TAG,
                 TextUtils.formatSimple(
@@ -1070,7 +1110,8 @@
         if (controller == null) {
             return;
         }
-        requestCreateController(controller, route, managerRequestId);
+        requestCreateController(controller, route, managerRequestId, transferInitiatorUserHandle,
+                transferInitiatorPackageName);
     }
 
     private List<MediaRoute2Info> getSortedRoutes(
@@ -1469,6 +1510,21 @@
         }
 
         /**
+         * Returns whether the transfer was requested by the calling app (as determined by comparing
+         * {@link UserHandle} and package name).
+         */
+        @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+        public boolean wasTransferRequestedBySelf() {
+            RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
+
+            UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+            String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+
+            return Objects.equals(android.os.Process.myUserHandle(), transferInitiatorUserHandle)
+                    && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+        }
+
+        /**
          * Returns the current {@link RoutingSessionInfo} associated to this controller.
          */
         @NonNull
@@ -1980,14 +2036,20 @@
 
         @Override
         public void requestCreateSessionByManager(
-                long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) {
+                long managerRequestId,
+                RoutingSessionInfo oldSession,
+                MediaRoute2Info route,
+                UserHandle transferInitiatorUserHandle,
+                String transferInitiatorPackageName) {
             mHandler.sendMessage(
                     obtainMessage(
                             MediaRouter2::onRequestCreateControllerByManagerOnHandler,
                             MediaRouter2.this,
                             oldSession,
                             route,
-                            managerRequestId));
+                            managerRequestId,
+                            transferInitiatorUserHandle,
+                            transferInitiatorPackageName));
         }
     }
 
@@ -2027,7 +2089,11 @@
 
         void stop();
 
-        void transfer(RoutingSessionInfo sessionInfo, MediaRoute2Info route);
+        void transfer(
+                @NonNull RoutingSessionInfo sessionInfo,
+                @NonNull MediaRoute2Info route,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName);
 
         List<RoutingController> getControllers();
 
@@ -2220,7 +2286,11 @@
 
             List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
             RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
-            transfer(targetSession, route);
+            transfer(
+                    targetSession,
+                    route,
+                    android.os.Process.myUserHandle(),
+                    mContext.getPackageName());
         }
 
         @Override
@@ -2243,14 +2313,24 @@
          *
          * @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer.
          * @param route The {@link MediaRoute2Info route} to transfer to.
-         * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info)
+         * @param transferInitiatorUserHandle The user handle of the app that initiated the
+         *     transfer.
+         * @param transferInitiatorPackageName The package name if of the app that initiated the
+         *     transfer.
+         * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String)
          * @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)
          */
         @Override
+        @SuppressWarnings("AndroidFrameworkRequiresPermission")
         public void transfer(
-                @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
+                @NonNull RoutingSessionInfo sessionInfo,
+                @NonNull MediaRoute2Info route,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName) {
             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
             Objects.requireNonNull(route, "route must not be null");
+            Objects.requireNonNull(transferInitiatorUserHandle);
+            Objects.requireNonNull(transferInitiatorPackageName);
 
             Log.v(
                     TAG,
@@ -2268,9 +2348,14 @@
             }
 
             if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
-                transferToRoute(sessionInfo, route);
+                transferToRoute(
+                        sessionInfo,
+                        route,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             } else {
-                requestCreateSession(sessionInfo, route);
+                requestCreateSession(sessionInfo, route, transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             }
         }
 
@@ -2282,21 +2367,30 @@
          * RoutingSessionInfo routing session's} {@link RoutingSessionInfo#getTransferableRoutes()
          * transferable routes list}. Otherwise, the request will fail.
          *
-         * <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request
-         * an out-of-session transfer.
+         * <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request an
+         * out-of-session transfer.
          *
          * @param session The {@link RoutingSessionInfo routing session} to transfer.
          * @param route The {@link MediaRoute2Info route} to transfer to. Must be one of the {@link
          *     RoutingSessionInfo routing session's} {@link
          *     RoutingSessionInfo#getTransferableRoutes() transferable routes}.
          */
+        @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
         private void transferToRoute(
-                @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) {
+                @NonNull RoutingSessionInfo session,
+                @NonNull MediaRoute2Info route,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName) {
             int requestId = createTransferRequest(session, route);
 
             try {
                 mMediaRouterService.transferToRouteWithManager(
-                        mClient, requestId, session.getId(), route);
+                        mClient,
+                        requestId,
+                        session.getId(),
+                        route,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             } catch (RemoteException ex) {
                 throw ex.rethrowFromSystemServer();
             }
@@ -2317,7 +2411,10 @@
          * @param route The {@link MediaRoute2Info route} to transfer to.
          */
         private void requestCreateSession(
-                @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
+                @NonNull RoutingSessionInfo oldSession,
+                @NonNull MediaRoute2Info route,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName) {
             if (TextUtils.isEmpty(oldSession.getClientPackageName())) {
                 Log.w(TAG, "requestCreateSession: Can't create a session without package name.");
                 this.onTransferFailed(oldSession, route);
@@ -2328,7 +2425,12 @@
 
             try {
                 mMediaRouterService.requestCreateSessionWithManager(
-                        mClient, requestId, oldSession, route);
+                        mClient,
+                        requestId,
+                        oldSession,
+                        route,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             } catch (RemoteException ex) {
                 throw ex.rethrowFromSystemServer();
             }
@@ -3055,7 +3157,8 @@
                 return;
             }
 
-            requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE);
+            requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE,
+                    android.os.Process.myUserHandle(), mContext.getPackageName());
         }
 
         @Override
@@ -3071,7 +3174,11 @@
          * #transferTo(MediaRoute2Info)}.
          */
         @Override
-        public void transfer(RoutingSessionInfo sessionInfo, MediaRoute2Info route) {
+        public void transfer(
+                @NonNull RoutingSessionInfo sessionInfo,
+                @NonNull MediaRoute2Info route,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName) {
             // Do nothing.
         }
 
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 830708c..06c0996 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -18,9 +18,11 @@
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 
+import android.Manifest;
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.content.Context;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
@@ -28,6 +30,7 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -467,30 +470,42 @@
      * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing
      * session based on the provided package name.
      */
-    public void transfer(@NonNull String packageName, @NonNull MediaRoute2Info route) {
+    @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
+    public void transfer(
+            @NonNull String packageName,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle userHandle) {
         Objects.requireNonNull(packageName, "packageName must not be null");
         Objects.requireNonNull(route, "route must not be null");
 
         List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName);
         RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
-        transfer(targetSession, route);
+        transfer(targetSession, route, userHandle, packageName);
     }
 
     /**
      * Transfers a routing session to a media route.
+     *
      * <p>{@link Callback#onTransferred} or {@link Callback#onTransferFailed} will be called
      * depending on the result.
      *
      * @param sessionInfo the routing session info to transfer
      * @param route the route transfer to
-     *
+     * @param transferInitiatorUserHandle the user handle of an app initiated the transfer
+     * @param transferInitiatorPackageName the package name of an app initiated the transfer
      * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo)
      * @see Callback#onTransferFailed(RoutingSessionInfo, MediaRoute2Info)
      */
-    public void transfer(@NonNull RoutingSessionInfo sessionInfo,
-            @NonNull MediaRoute2Info route) {
+    @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
+    public void transfer(
+            @NonNull RoutingSessionInfo sessionInfo,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
         Objects.requireNonNull(route, "route must not be null");
+        Objects.requireNonNull(transferInitiatorUserHandle);
+        Objects.requireNonNull(transferInitiatorPackageName);
 
         Log.v(TAG, "Transferring routing session. session= " + sessionInfo + ", route=" + route);
 
@@ -503,9 +518,11 @@
         }
 
         if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
-            transferToRoute(sessionInfo, route);
+            transferToRoute(
+                    sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName);
         } else {
-            requestCreateSession(sessionInfo, route);
+            requestCreateSession(sessionInfo, route, transferInitiatorUserHandle,
+                    transferInitiatorPackageName);
         }
     }
 
@@ -873,19 +890,30 @@
      *
      * @hide
      */
-    private void transferToRoute(@NonNull RoutingSessionInfo session,
-            @NonNull MediaRoute2Info route) {
+    @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
+    private void transferToRoute(
+            @NonNull RoutingSessionInfo session,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         int requestId = createTransferRequest(session, route);
 
         try {
             mMediaRouterService.transferToRouteWithManager(
-                    mClient, requestId, session.getId(), route);
+                    mClient,
+                    requestId,
+                    session.getId(),
+                    route,
+                    transferInitiatorUserHandle,
+                    transferInitiatorPackageName);
         } catch (RemoteException ex) {
             throw ex.rethrowFromSystemServer();
         }
     }
 
-    private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route) {
+    private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiationPackageName) {
         if (TextUtils.isEmpty(oldSession.getClientPackageName())) {
             Log.w(TAG, "requestCreateSession: Can't create a session without package name.");
             notifyTransferFailed(oldSession, route);
@@ -896,7 +924,8 @@
 
         try {
             mMediaRouterService.requestCreateSessionWithManager(
-                    mClient, requestId, oldSession, route);
+                    mClient, requestId, oldSession, route, transferInitiatorUserHandle,
+                    transferInitiationPackageName);
         } catch (RemoteException ex) {
             throw ex.rethrowFromSystemServer();
         }
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index a77c943..d28c26d 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -16,18 +16,25 @@
 
 package android.media;
 
+import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.UserHandle;
 import android.text.TextUtils;
 
 import com.android.internal.util.Preconditions;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -55,6 +62,33 @@
     private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
     private static final String KEY_VOLUME_HANDLING = "volumeHandling";
 
+    /**
+     * Indicates that the transfer happened by the default logic without explicit system's or user's
+     * request.
+     *
+     * <p>For example, an automatically connected Bluetooth device will have this transfer reason.
+     */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public static final int TRANSFER_REASON_FALLBACK = 0;
+
+    /** Indicates that the transfer happened from within a privileged application. */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1;
+
+    /** Indicates that the transfer happened from a non-privileged app. */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    public static final int TRANSFER_REASON_APP = 2;
+
+    /**
+     * Indicates the transfer reason.
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    @IntDef(value = {TRANSFER_REASON_FALLBACK, TRANSFER_REASON_SYSTEM_REQUEST, TRANSFER_REASON_APP})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TransferReason {}
+
     @NonNull
     final String mId;
     @Nullable
@@ -82,6 +116,10 @@
     final Bundle mControlHints;
     final boolean mIsSystemSession;
 
+    @TransferReason final int mTransferReason;
+
+    @Nullable final UserHandle mTransferInitiatorUserHandle;
+    @Nullable final String mTransferInitiatorPackageName;
 
     RoutingSessionInfo(@NonNull Builder builder) {
         Objects.requireNonNull(builder, "builder must not be null.");
@@ -116,6 +154,9 @@
                         volumeAdjustmentForRemoteGroupSessions);
 
         mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
+        mTransferReason = builder.mTransferReason;
+        mTransferInitiatorUserHandle = builder.mTransferInitiatorUserHandle;
+        mTransferInitiatorPackageName = builder.mTransferInitiatorPackageName;
     }
 
     RoutingSessionInfo(@NonNull Parcel src) {
@@ -140,6 +181,9 @@
 
         mControlHints = src.readBundle();
         mIsSystemSession = src.readBoolean();
+        mTransferReason = src.readInt();
+        mTransferInitiatorUserHandle = src.readParcelable(null, android.os.UserHandle.class);
+        mTransferInitiatorPackageName = src.readString();
     }
 
     @Nullable
@@ -330,6 +374,27 @@
         return mIsSystemSession;
     }
 
+    /** Returns the transfer reason for this routing session. */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    @TransferReason
+    public int getTransferReason() {
+        return mTransferReason;
+    }
+
+    /** @hide */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    @Nullable
+    public UserHandle getTransferInitiatorUserHandle() {
+        return mTransferInitiatorUserHandle;
+    }
+
+    /** @hide */
+    @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+    @Nullable
+    public String getTransferInitiatorPackageName() {
+        return mTransferInitiatorPackageName;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -351,6 +416,13 @@
         dest.writeInt(mVolume);
         dest.writeBundle(mControlHints);
         dest.writeBoolean(mIsSystemSession);
+        dest.writeInt(mTransferReason);
+        if (mTransferInitiatorUserHandle != null) {
+            mTransferInitiatorUserHandle.writeToParcel(dest, /* flags= */ 0);
+        } else {
+            dest.writeParcelable(null, /* flags= */ 0);
+        }
+        dest.writeString(mTransferInitiatorPackageName);
     }
 
     /**
@@ -379,6 +451,9 @@
         pw.println(indent + "mVolume=" + mVolume);
         pw.println(indent + "mControlHints=" + mControlHints);
         pw.println(indent + "mIsSystemSession=" + mIsSystemSession);
+        pw.println(indent + "mTransferReason=" + mTransferReason);
+        pw.println(indent + "mtransferInitiatorUserHandle=" + mTransferInitiatorUserHandle);
+        pw.println(indent + "mtransferInitiatorPackageName=" + mTransferInitiatorPackageName);
     }
 
     @Override
@@ -406,39 +481,69 @@
                 && Objects.equals(mTransferableRoutes, other.mTransferableRoutes)
                 && (mVolumeHandling == other.mVolumeHandling)
                 && (mVolumeMax == other.mVolumeMax)
-                && (mVolume == other.mVolume);
+                && (mVolume == other.mVolume)
+                && (mTransferReason == other.mTransferReason)
+                && Objects.equals(mTransferInitiatorUserHandle, other.mTransferInitiatorUserHandle)
+                && Objects.equals(
+                mTransferInitiatorPackageName, other.mTransferInitiatorPackageName);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mId, mName, mOwnerPackageName, mClientPackageName, mProviderId,
-                mSelectedRoutes, mSelectableRoutes, mDeselectableRoutes, mTransferableRoutes,
-                mVolumeMax, mVolumeHandling, mVolume);
+        return Objects.hash(
+                mId,
+                mName,
+                mOwnerPackageName,
+                mClientPackageName,
+                mProviderId,
+                mSelectedRoutes,
+                mSelectableRoutes,
+                mDeselectableRoutes,
+                mTransferableRoutes,
+                mVolumeMax,
+                mVolumeHandling,
+                mVolume,
+                mTransferReason,
+                mTransferInitiatorUserHandle,
+                mTransferInitiatorPackageName);
     }
 
     @Override
     public String toString() {
-        StringBuilder result = new StringBuilder()
-                .append("RoutingSessionInfo{ ")
-                .append("sessionId=").append(getId())
-                .append(", name=").append(getName())
-                .append(", clientPackageName=").append(getClientPackageName())
-                .append(", selectedRoutes={")
-                .append(String.join(",", getSelectedRoutes()))
-                .append("}")
-                .append(", selectableRoutes={")
-                .append(String.join(",", getSelectableRoutes()))
-                .append("}")
-                .append(", deselectableRoutes={")
-                .append(String.join(",", getDeselectableRoutes()))
-                .append("}")
-                .append(", transferableRoutes={")
-                .append(String.join(",", getTransferableRoutes()))
-                .append("}")
-                .append(", volumeHandling=").append(getVolumeHandling())
-                .append(", volumeMax=").append(getVolumeMax())
-                .append(", volume=").append(getVolume())
-                .append(" }");
+        StringBuilder result =
+                new StringBuilder()
+                        .append("RoutingSessionInfo{ ")
+                        .append("sessionId=")
+                        .append(getId())
+                        .append(", name=")
+                        .append(getName())
+                        .append(", clientPackageName=")
+                        .append(getClientPackageName())
+                        .append(", selectedRoutes={")
+                        .append(String.join(",", getSelectedRoutes()))
+                        .append("}")
+                        .append(", selectableRoutes={")
+                        .append(String.join(",", getSelectableRoutes()))
+                        .append("}")
+                        .append(", deselectableRoutes={")
+                        .append(String.join(",", getDeselectableRoutes()))
+                        .append("}")
+                        .append(", transferableRoutes={")
+                        .append(String.join(",", getTransferableRoutes()))
+                        .append("}")
+                        .append(", volumeHandling=")
+                        .append(getVolumeHandling())
+                        .append(", volumeMax=")
+                        .append(getVolumeMax())
+                        .append(", volume=")
+                        .append(getVolume())
+                        .append(", transferReason=")
+                        .append(getTransferReason())
+                        .append(", transferInitiatorUserHandle=")
+                        .append(getTransferInitiatorUserHandle())
+                        .append(", transferInitiatorPackageName=")
+                        .append(getTransferInitiatorPackageName())
+                        .append(" }");
         return result.toString();
     }
 
@@ -494,6 +599,9 @@
         @Nullable
         private Bundle mControlHints;
         private boolean mIsSystemSession;
+        @TransferReason private int mTransferReason = TRANSFER_REASON_FALLBACK;
+        @Nullable private UserHandle mTransferInitiatorUserHandle;
+        @Nullable private String mTransferInitiatorPackageName;
 
         /**
          * Constructor for builder to create {@link RoutingSessionInfo}.
@@ -555,6 +663,9 @@
 
             mControlHints = sessionInfo.mControlHints;
             mIsSystemSession = sessionInfo.mIsSystemSession;
+            mTransferReason = sessionInfo.mTransferReason;
+            mTransferInitiatorUserHandle = sessionInfo.mTransferInitiatorUserHandle;
+            mTransferInitiatorPackageName = sessionInfo.mTransferInitiatorPackageName;
         }
 
         /**
@@ -784,6 +895,35 @@
         }
 
         /**
+         * Sets transfer reason for the current session.
+         *
+         * <p>By default the transfer reason is set to {@link
+         * RoutingSessionInfo#TRANSFER_REASON_FALLBACK}.
+         */
+        @NonNull
+        @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+        public Builder setTransferReason(@TransferReason int transferReason) {
+            mTransferReason = transferReason;
+            return this;
+        }
+
+        /**
+         * Sets the user handle and package name of the process that initiated the transfer.
+         *
+         * <p>By default the transfer initiation user handle and package name are set to {@code
+         * null}.
+         */
+        @NonNull
+        @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
+        public Builder setTransferInitiator(
+                @Nullable UserHandle transferInitiatorUserHandle,
+                @Nullable String transferInitiatorPackageName) {
+            mTransferInitiatorUserHandle = transferInitiatorUserHandle;
+            mTransferInitiatorPackageName = transferInitiatorPackageName;
+            return this;
+        }
+
+        /**
          * Builds a routing session info.
          *
          * @throws IllegalArgumentException if no selected routes are added.
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index e168498..b85decc 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -16,6 +16,8 @@
 
 package android.media.audiopolicy;
 
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -34,6 +36,7 @@
 import android.media.AudioManager;
 import android.media.AudioRecord;
 import android.media.AudioTrack;
+import android.media.FadeManagerConfiguration;
 import android.media.IAudioService;
 import android.media.MediaRecorder;
 import android.media.projection.MediaProjection;
@@ -49,6 +52,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -612,6 +616,110 @@
         return mRegistrationId;
     }
 
+    /**
+     * Sets a custom {@link FadeManagerConfiguration} to handle fade cycle of players during
+     * {@link android.media.AudioManager#AUDIOFOCUS_LOSS}
+     *
+     * @param fmcForFocusLoss custom {@link FadeManagerConfiguration}
+     * @return {@link AudioManager#SUCCESS} if the update was successful,
+     *     {@link AudioManager#ERROR} otherwise
+     * @throws IllegalStateException if the audio policy is not registered
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    @SystemApi
+    public int setFadeManagerConfigurationForFocusLoss(
+            @NonNull FadeManagerConfiguration fmcForFocusLoss) {
+        Objects.requireNonNull(fmcForFocusLoss,
+                "FadeManagerConfiguration for focus loss cannot be null");
+
+        IAudioService service = getService();
+        synchronized (mLock) {
+            Preconditions.checkState(isAudioPolicyRegisteredLocked(),
+                    "Cannot set FadeManagerConfiguration with unregistered AudioPolicy");
+
+            try {
+                return service.setFadeManagerConfigurationForFocusLoss(fmcForFocusLoss);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received remote exception for setFadeManagerConfigurationForFocusLoss:",
+                        e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Clear the current {@link FadeManagerConfiguration} set to handle fade cycles of players
+     * during {@link android.media.AudioManager#AUDIOFOCUS_LOSS}
+     *
+     * <p>In the absence of custom {@link FadeManagerConfiguration}, the default configurations will
+     * be used to handle fade cycles during audio focus loss.
+     *
+     * @return {@link AudioManager#SUCCESS} if the update was successful,
+     *     {@link AudioManager#ERROR} otherwise
+     * @throws IllegalStateException if the audio policy is not registered
+     * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    @SystemApi
+    public int clearFadeManagerConfigurationForFocusLoss() {
+        IAudioService service = getService();
+        synchronized (mLock) {
+            Preconditions.checkState(isAudioPolicyRegisteredLocked(),
+                    "Cannot clear FadeManagerConfiguration from unregistered AudioPolicy");
+
+            try {
+                return service.clearFadeManagerConfigurationForFocusLoss();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received remote exception for "
+                                + "clearFadeManagerConfigurationForFocusLoss:", e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Get the current fade manager configuration used for fade operations during
+     * {@link android.media.AudioManager#AUDIOFOCUS_LOSS}
+     *
+     * <p>If no custom {@link FadeManagerConfiguration} is set, the default configuration currently
+     * active will be returned.
+     *
+     * @return the active {@link FadeManagerConfiguration} used during audio focus loss
+     * @throws IllegalStateException if the audio policy is not registered
+     * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)
+     * @see #clearFadeManagerConfigurationForFocusLoss()
+     * @hide
+     */
+    @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION)
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    @SystemApi
+    @NonNull
+    public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() {
+        IAudioService service = getService();
+        synchronized (mLock) {
+            Preconditions.checkState(isAudioPolicyRegisteredLocked(),
+                    "Cannot get FadeManagerConfiguration from unregistered AudioPolicy");
+
+            try {
+                return service.getFadeManagerConfigurationForFocusLoss();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Received remote exception for getFadeManagerConfigurationForFocusLoss:",
+                        e);
+                throw e.rethrowFromSystemServer();
+
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isAudioPolicyRegisteredLocked() {
+        return mStatus == POLICY_STATUS_REGISTERED;
+    }
+
     private boolean policyReadyToUse() {
         synchronized (mLock) {
             if (mStatus != POLICY_STATUS_REGISTERED) {
diff --git a/media/java/android/media/flags/fade_manager_configuration.aconfig b/media/java/android/media/flags/fade_manager_configuration.aconfig
deleted file mode 100644
index 100e2235..0000000
--- a/media/java/android/media/flags/fade_manager_configuration.aconfig
+++ /dev/null
@@ -1,8 +0,0 @@
-package: "com.android.media.flags"
-
-flag {
-    namespace: "media_solutions"
-    name: "enable_fade_manager_configuration"
-    description: "Enable Fade Manager Configuration support to determine fade properties"
-    bug: "307354764"
-}
\ No newline at end of file
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 07f63e5..7f95886 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -69,3 +69,17 @@
     description: "Use BluetoothDevice.getAlias to populate the name of Bluetooth MediaRoute2Infos."
     bug: "314324170"
 }
+
+flag {
+     name: "enable_built_in_speaker_route_suitability_statuses"
+     namespace: "media_solutions"
+     description: "Make MediaRoute2Info provide information about routes suitability for transfer."
+     bug: "279555229"
+}
+
+flag {
+    name: "enable_notifying_activity_manager_with_media_session_status_change"
+    namespace: "media_solutions"
+    description: "Notify ActivityManager with the changes in playback state of the media session."
+    bug: "295518668"
+}
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
index 7891ee6..60497fe 100644
--- a/media/java/android/media/session/PlaybackState.java
+++ b/media/java/android/media/session/PlaybackState.java
@@ -524,6 +524,28 @@
         return false;
     }
 
+    /**
+     * Returns whether the service holding the media session should run in the foreground when the
+     * media session has this playback state or not.
+     *
+     * @hide
+     */
+    public boolean shouldAllowServiceToRunInForeground() {
+        switch (mState) {
+            case PlaybackState.STATE_PLAYING:
+            case PlaybackState.STATE_FAST_FORWARDING:
+            case PlaybackState.STATE_REWINDING:
+            case PlaybackState.STATE_BUFFERING:
+            case PlaybackState.STATE_CONNECTING:
+            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+            case PlaybackState.STATE_SKIPPING_TO_NEXT:
+            case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     public static final @android.annotation.NonNull Parcelable.Creator<PlaybackState> CREATOR =
             new Parcelable.Creator<PlaybackState>() {
         @Override
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 2da9b9a..3fcb871 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -3716,7 +3716,7 @@
                     "([Landroid/media/tv/tuner/filter/FilterEvent;)V");
 
     jclass sharedFilterClazz = env->FindClass("android/media/tv/tuner/filter/SharedFilter");
-    gFields.sharedFilterContext = env->GetFieldID(filterClazz, "mNativeContext", "J");
+    gFields.sharedFilterContext = env->GetFieldID(sharedFilterClazz, "mNativeContext", "J");
     gFields.sharedFilterInitID = env->GetMethodID(sharedFilterClazz, "<init>", "()V");
     gFields.onSharedFilterStatusID = env->GetMethodID(sharedFilterClazz, "onFilterStatus", "(I)V");
     gFields.onSharedFilterEventID =
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
index fb6bd48..f105ae9 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
@@ -16,7 +16,7 @@
 
 package com.android.audiopolicytest;
 
-import static com.android.media.flags.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import static org.junit.Assert.assertThrows;
 
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
index 8ed4bf2..c836df3 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
@@ -385,7 +385,9 @@
         MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1);
         assertThat(routeToSelect).isNotNull();
 
-        mManager.transfer(mPackageName, routeToSelect);
+        mManager.transfer(
+                mPackageName, routeToSelect,
+                android.os.Process.myUserHandle());
         assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
         assertThat(mManager.getRemoteSessions()).hasSize(1);
     }
@@ -411,7 +413,9 @@
 
         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
 
-        mManager.transfer(mPackageName, routeToSelect);
+        mManager.transfer(
+                mPackageName, routeToSelect,
+                android.os.Process.myUserHandle());
         assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
@@ -450,7 +454,11 @@
                 .addFeature(FEATURE_REMOTE_PLAYBACK)
                 .build();
 
-        mManager.transfer(mManager.getSystemRoutingSession(null), unknownRoute);
+        mManager.transfer(
+                mManager.getSystemRoutingSession(null),
+                unknownRoute,
+                android.os.Process.myUserHandle(),
+                mContext.getPackageName());
         assertThat(onSessionCreatedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
         assertThat(onTransferFailedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
     }
@@ -484,7 +492,11 @@
         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
         assertThat(mRouter2.getControllers()).hasSize(1);
 
-        mManager.transfer(mManager.getRoutingSessions(mPackageName).get(0), routeToSelect);
+        mManager.transfer(
+                mManager.getRoutingSessions(mPackageName).get(0),
+                routeToSelect,
+                android.os.Process.myUserHandle(),
+                mContext.getPackageName());
         assertThat(transferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(2);
@@ -516,7 +528,11 @@
             }
         });
         awaitOnRouteChangedManager(
-                () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID1)),
+                () ->
+                        mManager.transfer(
+                                mPackageName,
+                                routes.get(ROUTE_ID1),
+                                android.os.Process.myUserHandle()),
                 ROUTE_ID1,
                 route -> TextUtils.equals(route.getClientPackageName(), mPackageName));
         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
@@ -527,7 +543,11 @@
         RoutingSessionInfo sessionInfo = sessions.get(1);
 
         awaitOnRouteChangedManager(
-                () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)),
+                () ->
+                        mManager.transfer(
+                                mPackageName,
+                                routes.get(ROUTE_ID5_TO_TRANSFER_TO),
+                                android.os.Process.myUserHandle()),
                 ROUTE_ID5_TO_TRANSFER_TO,
                 route -> TextUtils.equals(route.getClientPackageName(), mPackageName));
 
@@ -585,9 +605,11 @@
         assertThat(route1).isNotNull();
         assertThat(route2).isNotNull();
 
-        mManager.transfer(mPackageName, route1);
+        mManager.transfer(
+                mPackageName, route1, android.os.Process.myUserHandle());
         assertThat(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
-        mManager.transfer(mPackageName, route2);
+        mManager.transfer(
+                mPackageName, route2, android.os.Process.myUserHandle());
         assertThat(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         // onTransferFailed/onSessionReleased should not be called.
@@ -634,7 +656,11 @@
 
         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
         RoutingSessionInfo targetSession = sessions.get(sessions.size() - 1);
-        mManager.transfer(targetSession, routes.get(ROUTE_ID6_TO_BE_IGNORED));
+        mManager.transfer(
+                targetSession,
+                routes.get(ROUTE_ID6_TO_BE_IGNORED),
+                android.os.Process.myUserHandle(),
+                mContext.getPackageName());
 
         assertThat(onSessionCreatedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
         assertThat(onFailedLatch.await(MediaRouter2Manager.TRANSFER_TIMEOUT_MS,
@@ -705,7 +731,10 @@
             }
         });
 
-        mManager.transfer(mPackageName, routes.get(ROUTE_ID1));
+        mManager.transfer(
+                mPackageName,
+                routes.get(ROUTE_ID1),
+                android.os.Process.myUserHandle());
         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
@@ -860,7 +889,8 @@
         });
 
         mRouter2.setOnGetControllerHintsListener(listener);
-        mManager.transfer(mPackageName, route);
+        mManager.transfer(
+                mPackageName, route, android.os.Process.myUserHandle());
         assertThat(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
         assertThat(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
@@ -905,7 +935,10 @@
             }
         });
 
-        mManager.transfer(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+        mManager.transfer(
+                mPackageName,
+                routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT),
+                android.os.Process.myUserHandle());
         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
     }
 
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index c4c8128..abe4a3d 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -18,10 +18,12 @@
 
 #include <aidl/android/hardware/power/SessionHint.h>
 #include <aidl/android/hardware/power/SessionMode.h>
+#include <android-base/stringprintf.h>
 #include <android/WorkDuration.h>
 #include <android/os/IHintManager.h>
 #include <android/os/IHintSession.h>
 #include <android/performance_hint.h>
+#include <android/trace.h>
 #include <binder/Binder.h>
 #include <binder/IBinder.h>
 #include <binder/IServiceManager.h>
@@ -30,6 +32,7 @@
 #include <utils/SystemClock.h>
 
 #include <chrono>
+#include <set>
 #include <utility>
 #include <vector>
 
@@ -40,6 +43,7 @@
 
 using AidlSessionHint = aidl::android::hardware::power::SessionHint;
 using AidlSessionMode = aidl::android::hardware::power::SessionMode;
+using android::base::StringPrintf;
 
 struct APerformanceHintSession;
 
@@ -98,10 +102,21 @@
     std::vector<int64_t> mLastHintSentTimestamp;
     // Cached samples
     std::vector<WorkDuration> mActualWorkDurations;
+    std::string mSessionName;
+    static int32_t sIDCounter;
+    // The most recent set of thread IDs
+    std::vector<int32_t> mLastThreadIDs;
+    // Tracing helpers
+    void traceThreads(std::vector<int32_t>& tids);
+    void tracePowerEfficient(bool powerEfficient);
+    void traceActualDuration(int64_t actualDuration);
+    void traceBatchSize(size_t batchSize);
+    void traceTargetDuration(int64_t targetDuration);
 };
 
 static IHintManager* gIHintManagerForTesting = nullptr;
 static APerformanceHintManager* gHintManagerForTesting = nullptr;
+int32_t APerformanceHintSession::sIDCounter = 0;
 
 // ===================================== APerformanceHintManager implementation
 APerformanceHintManager::APerformanceHintManager(sp<IHintManager> manager,
@@ -150,8 +165,12 @@
     if (!ret.isOk() || !session) {
         return nullptr;
     }
-    return new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
-                                       initialTargetWorkDurationNanos);
+    auto out = new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
+                                           initialTargetWorkDurationNanos);
+    out->traceThreads(tids);
+    out->traceTargetDuration(initialTargetWorkDurationNanos);
+    out->tracePowerEfficient(false);
+    return out;
 }
 
 int64_t APerformanceHintManager::getPreferredRateNanos() const {
@@ -174,6 +193,7 @@
                                                         ndk::enum_range<AidlSessionHint>().end()};
 
     mLastHintSentTimestamp = std::vector<int64_t>(sessionHintRange.size(), 0);
+    mSessionName = android::base::StringPrintf("ADPF Session %" PRId32, ++sIDCounter);
 }
 
 APerformanceHintSession::~APerformanceHintSession() {
@@ -200,6 +220,8 @@
      * as they are most likely obsolete.
      */
     mActualWorkDurations.clear();
+    traceBatchSize(0);
+    traceTargetDuration(targetDurationNanos);
     mFirstTargetMetTimestamp = 0;
     mLastTargetMetTimestamp = 0;
     return 0;
@@ -254,6 +276,9 @@
         }
         return EPIPE;
     }
+
+    traceThreads(tids);
+
     return 0;
 }
 
@@ -289,6 +314,7 @@
               ret.exceptionMessage().c_str());
         return EPIPE;
     }
+    tracePowerEfficient(enabled);
     return OK;
 }
 
@@ -318,6 +344,7 @@
     int64_t actualTotalDurationNanos = workDuration->actualTotalDurationNanos;
     int64_t now = uptimeNanos();
     workDuration->timestampNanos = now;
+    traceActualDuration(workDuration->actualTotalDurationNanos);
     mActualWorkDurations.push_back(std::move(*workDuration));
 
     if (actualTotalDurationNanos >= mTargetDurationNanos) {
@@ -335,6 +362,7 @@
          */
         if (now - mFirstTargetMetTimestamp > mPreferredRateNanos &&
             now - mLastTargetMetTimestamp <= mPreferredRateNanos) {
+            traceBatchSize(mActualWorkDurations.size());
             return 0;
         }
         mLastTargetMetTimestamp = now;
@@ -346,12 +374,54 @@
               ret.exceptionMessage().c_str());
         mFirstTargetMetTimestamp = 0;
         mLastTargetMetTimestamp = 0;
+        traceBatchSize(mActualWorkDurations.size());
         return ret.exceptionCode() == binder::Status::EX_ILLEGAL_ARGUMENT ? EINVAL : EPIPE;
     }
     mActualWorkDurations.clear();
+    traceBatchSize(0);
 
     return 0;
 }
+// ===================================== Tracing helpers
+
+void APerformanceHintSession::traceThreads(std::vector<int32_t>& tids) {
+    std::set<int32_t> tidSet{tids.begin(), tids.end()};
+
+    // Disable old TID tracing
+    for (int32_t tid : mLastThreadIDs) {
+        if (!tidSet.count(tid)) {
+            std::string traceName =
+                    android::base::StringPrintf("%s TID: %" PRId32, mSessionName.c_str(), tid);
+            ATrace_setCounter(traceName.c_str(), 0);
+        }
+    }
+
+    // Add new TID tracing
+    for (int32_t tid : tids) {
+        std::string traceName =
+                android::base::StringPrintf("%s TID: %" PRId32, mSessionName.c_str(), tid);
+        ATrace_setCounter(traceName.c_str(), 1);
+    }
+
+    mLastThreadIDs = std::move(tids);
+}
+
+void APerformanceHintSession::tracePowerEfficient(bool powerEfficient) {
+    ATrace_setCounter((mSessionName + " power efficiency mode").c_str(), powerEfficient);
+}
+
+void APerformanceHintSession::traceActualDuration(int64_t actualDuration) {
+    ATrace_setCounter((mSessionName + " actual duration").c_str(), actualDuration);
+}
+
+void APerformanceHintSession::traceBatchSize(size_t batchSize) {
+    std::string traceName = StringPrintf("%s batch size", mSessionName.c_str());
+    ATrace_setCounter((mSessionName + " batch size").c_str(), batchSize);
+}
+
+void APerformanceHintSession::traceTargetDuration(int64_t targetDuration) {
+    ATrace_setCounter((mSessionName + " target duration").c_str(), targetDuration);
+}
 
 // ===================================== C API
 APerformanceHintManager* APerformanceHint_getManager() {
diff --git a/packages/SettingsLib/AdaptiveIcon/Android.bp b/packages/SettingsLib/AdaptiveIcon/Android.bp
index df72a92..044ba87 100644
--- a/packages/SettingsLib/AdaptiveIcon/Android.bp
+++ b/packages/SettingsLib/AdaptiveIcon/Android.bp
@@ -15,9 +15,12 @@
     resource_dirs: ["res"],
 
     static_libs: [
-          "androidx.annotation_annotation",
-          "SettingsLibTile"
+        "androidx.annotation_annotation",
+        "SettingsLibTile",
     ],
 
     min_sdk_version: "21",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index b9dc618..ffe3d1d 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -48,6 +48,7 @@
         "SettingsLibTwoTargetPreference",
         "SettingsLibUsageProgressBarPreference",
         "SettingsLibUtils",
+        "settingslib_media_flags_lib",
         "settingslib_flags_lib",
     ],
 
@@ -59,6 +60,9 @@
         "src/**/*.java",
         "src/**/*.kt",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // NOTE: Keep this module in sync with ./common.mk
@@ -78,6 +82,19 @@
 }
 
 java_aconfig_library {
-    name: "settingslib_flags_lib",
+    name: "settingslib_media_flags_lib",
     aconfig_declarations: "settingslib_media_flags",
 }
+
+aconfig_declarations {
+    name: "settingslib_flags",
+    package: "com.android.settingslib.flags",
+    srcs: [
+        "aconfig/settingslib.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "settingslib_flags_lib",
+    aconfig_declarations: "settingslib_flags",
+}
diff --git a/packages/SettingsLib/EmergencyNumber/Android.bp b/packages/SettingsLib/EmergencyNumber/Android.bp
index 986baf7..bd0dbdc 100644
--- a/packages/SettingsLib/EmergencyNumber/Android.bp
+++ b/packages/SettingsLib/EmergencyNumber/Android.bp
@@ -17,4 +17,7 @@
     ],
     sdk_version: "system_current",
     min_sdk_version: "21",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/MainSwitchPreference/Android.bp b/packages/SettingsLib/MainSwitchPreference/Android.bp
index 010a6ce..d9f74da 100644
--- a/packages/SettingsLib/MainSwitchPreference/Android.bp
+++ b/packages/SettingsLib/MainSwitchPreference/Android.bp
@@ -28,4 +28,7 @@
         "com.android.extservices",
         "com.android.healthfitness",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/RestrictedLockUtils/Android.bp b/packages/SettingsLib/RestrictedLockUtils/Android.bp
index 028489d..3b04bd9 100644
--- a/packages/SettingsLib/RestrictedLockUtils/Android.bp
+++ b/packages/SettingsLib/RestrictedLockUtils/Android.bp
@@ -30,4 +30,7 @@
         "//apex_available:platform",
         "com.android.permission",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/SchedulesProvider/Android.bp b/packages/SettingsLib/SchedulesProvider/Android.bp
index 2d93e4e..22e4e94 100644
--- a/packages/SettingsLib/SchedulesProvider/Android.bp
+++ b/packages/SettingsLib/SchedulesProvider/Android.bp
@@ -14,9 +14,12 @@
     srcs: ["src/**/*.java"],
 
     static_libs: [
-          "androidx.annotation_annotation",
+        "androidx.annotation_annotation",
     ],
 
     sdk_version: "system_current",
     min_sdk_version: "21",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/SearchProvider/Android.bp b/packages/SettingsLib/SearchProvider/Android.bp
index c07a802..c385d38 100644
--- a/packages/SettingsLib/SearchProvider/Android.bp
+++ b/packages/SettingsLib/SearchProvider/Android.bp
@@ -15,4 +15,7 @@
 
     sdk_version: "system_current",
     min_sdk_version: "21",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
index 66bd6f5..d5cf1a35 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
@@ -26,6 +26,7 @@
 import androidx.compose.material.icons.outlined.PowerOff
 import androidx.compose.material.icons.outlined.Shield
 import androidx.compose.material.icons.outlined.WarningAmber
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -78,24 +79,19 @@
                 imageVector = Icons.Outlined.WarningAmber,
                 buttons = listOf(
                     CardButton(text = "Action") {},
-                    CardButton(text = "Action", isMain = true) {},
-                )
+                ),
+                tintColor = MaterialTheme.colorScheme.error,
+                containerColor = MaterialTheme.colorScheme.errorContainer,
             )
         )
     }
 
     @Composable
     private fun SettingsCardWithoutIcon() {
-        var isVisible by rememberSaveable { mutableStateOf(true) }
         SettingsCard(
             CardModel(
                 title = stringResource(R.string.sample_title),
                 text = stringResource(R.string.sample_text),
-                isVisible = { isVisible },
-                onDismiss = { isVisible = false },
-                buttons = listOf(
-                    CardButton(text = "Action") {},
-                ),
             )
         )
     }
@@ -104,6 +100,7 @@
     fun SampleSettingsCollapsibleCard() {
         val context = LocalContext.current
         var isVisible0 by rememberSaveable { mutableStateOf(true) }
+        var isVisible1 by rememberSaveable { mutableStateOf(true) }
         val cards = remember {
             mutableStateListOf(
                 CardModel(
@@ -114,16 +111,17 @@
                     onDismiss = { isVisible0 = false },
                     buttons = listOf(
                         CardButton(text = "Action") {},
-                    )
+                    ),
                 ),
                 CardModel(
                     title = context.getString(R.string.sample_title),
                     text = context.getString(R.string.sample_text),
                     imageVector = Icons.Outlined.Shield,
+                    isVisible = { isVisible1 },
+                    onDismiss = { isVisible1 = false },
                     buttons = listOf(
                         CardButton(text = "Action") {},
-                        CardButton(text = "Main action", isMain = true) {},
-                    )
+                    ),
                 )
             )
         }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index 993cb4a..c143390 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -37,7 +37,6 @@
     val itemPaddingAround = 8.dp
     val itemDividerHeight = 32.dp
 
-    val iconSmall = 16.dp
     val iconLarge = 48.dp
 
     /** The size when app icon is displayed in list. */
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
index b18a1bc..b2a8b87 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
@@ -16,11 +16,11 @@
 
 package com.android.settingslib.spa.widget.card
 
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 
 data class CardButton(
     val text: String,
-    val isMain: Boolean = false,
     val onClick: () -> Unit,
 )
 
@@ -38,4 +38,10 @@
     val onDismiss: (() -> Unit)? = null,
 
     val buttons: List<CardButton> = emptyList(),
+
+    /** If specified, this color will be used to tint the icon and the buttons. */
+    val tintColor: Color = Color.Unspecified,
+
+    /** If specified, this color will be used to tint the icon and the buttons. */
+    val containerColor: Color = Color.Unspecified,
 )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
index 7eec888..c7845fa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -23,26 +23,26 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Close
 import androidx.compose.material.icons.outlined.WarningAmber
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.Card
 import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
@@ -72,11 +72,14 @@
 }
 
 @Composable
-fun SettingsCardContent(content: @Composable ColumnScope.() -> Unit) {
+fun SettingsCardContent(
+    containerColor: Color = Color.Unspecified,
+    content: @Composable ColumnScope.() -> Unit,
+) {
     Card(
         shape = CornerExtraSmall,
         colors = CardDefaults.cardColors(
-            containerColor = SettingsTheme.colorScheme.surface,
+            containerColor = containerColor.takeOrElse { SettingsTheme.colorScheme.surface },
         ),
         modifier = Modifier
             .fillMaxWidth()
@@ -95,37 +98,43 @@
 @Composable
 internal fun SettingsCardImpl(model: CardModel) {
     AnimatedVisibility(visible = model.isVisible()) {
-        SettingsCardContent {
+        SettingsCardContent(containerColor = model.containerColor) {
             Column(
-                modifier = Modifier.padding(SettingsDimension.itemPaddingStart),
+                modifier = Modifier.padding(
+                    horizontal = SettingsDimension.dialogItemPaddingHorizontal,
+                    vertical = SettingsDimension.itemPaddingAround,
+                ),
                 verticalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround)
             ) {
-                CardHeader(model.imageVector, model.onDismiss)
+                CardHeader(model.imageVector, model.tintColor, model.onDismiss)
                 SettingsTitle(model.title)
                 SettingsBody(model.text)
-                Buttons(model.buttons)
+                Buttons(model.buttons, model.tintColor)
             }
         }
     }
 }
 
 @Composable
-fun CardHeader(imageVector: ImageVector?, onDismiss: (() -> Unit)? = null) {
+fun CardHeader(imageVector: ImageVector?, iconColor: Color, onDismiss: (() -> Unit)? = null) {
+    if (imageVector != null || onDismiss != null) {
+        Spacer(Modifier.height(SettingsDimension.buttonPaddingVertical))
+    }
     Row(Modifier.fillMaxWidth()) {
-        CardIcon(imageVector)
+        CardIcon(imageVector, iconColor)
         Spacer(modifier = Modifier.weight(1f))
         DismissButton(onDismiss)
     }
 }
 
 @Composable
-private fun CardIcon(imageVector: ImageVector?) {
+private fun CardIcon(imageVector: ImageVector?, color: Color) {
     if (imageVector != null) {
         Icon(
             imageVector = imageVector,
             contentDescription = null,
             modifier = Modifier.size(SettingsDimension.itemIconSize),
-            tint = MaterialTheme.colorScheme.primary,
+            tint = color.takeOrElse { MaterialTheme.colorScheme.primary },
         )
     }
 }
@@ -146,52 +155,35 @@
                 contentDescription = stringResource(
                     androidx.compose.material3.R.string.m3c_snackbar_dismiss
                 ),
-                modifier = Modifier.size(SettingsDimension.iconSmall),
+                modifier = Modifier.padding(SettingsDimension.paddingSmall),
             )
         }
     }
 }
 
 @Composable
-private fun Buttons(buttons: List<CardButton>) {
+private fun Buttons(buttons: List<CardButton>, color: Color) {
     if (buttons.isNotEmpty()) {
         Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(top = SettingsDimension.itemPaddingAround),
+            modifier = Modifier.fillMaxWidth(),
             horizontalArrangement = Arrangement.spacedBy(
                 space = SettingsDimension.itemPaddingEnd,
                 alignment = Alignment.End,
             ),
         ) {
             for (button in buttons) {
-                Button(button)
+                Button(button, color)
             }
         }
+    } else {
+        Spacer(Modifier.height(SettingsDimension.itemPaddingAround))
     }
 }
 
 @Composable
-private fun Button(button: CardButton) {
-    if (button.isMain) {
-        Button(
-            onClick = button.onClick,
-            colors = ButtonDefaults.buttonColors(
-                containerColor = SettingsTheme.colorScheme.primaryContainer,
-            ),
-        ) {
-            Text(
-                text = button.text,
-                color = SettingsTheme.colorScheme.onPrimaryContainer,
-            )
-        }
-    } else {
-        OutlinedButton(onClick = button.onClick) {
-            Text(
-                text = button.text,
-                color = MaterialTheme.colorScheme.onSurface,
-            )
-        }
+private fun Button(button: CardButton, color: Color) {
+    TextButton(onClick = button.onClick) {
+        Text(text = button.text, color = color)
     }
 }
 
@@ -206,7 +198,6 @@
                 imageVector = Icons.Outlined.WarningAmber,
                 buttons = listOf(
                     CardButton(text = "Action") {},
-                    CardButton(text = "Action", isMain = true) {},
                 )
             )
         )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt
index 6e36490..c34df65 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt
@@ -141,7 +141,6 @@
                     imageVector = Icons.Outlined.Shield,
                     buttons = listOf(
                         CardButton(text = "Action") {},
-                        CardButton(text = "Main action", isMain = true) {},
                     )
                 )
             )
diff --git a/packages/SettingsLib/Tile/Android.bp b/packages/SettingsLib/Tile/Android.bp
index eb9e329..19c59dd 100644
--- a/packages/SettingsLib/Tile/Android.bp
+++ b/packages/SettingsLib/Tile/Android.bp
@@ -14,8 +14,11 @@
     srcs: ["src/**/*.java"],
 
     static_libs: [
-          "androidx.annotation_annotation",
+        "androidx.annotation_annotation",
     ],
 
     min_sdk_version: "21",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/Utils/Android.bp b/packages/SettingsLib/Utils/Android.bp
index c7ad24c..d5a56c8 100644
--- a/packages/SettingsLib/Utils/Android.bp
+++ b/packages/SettingsLib/Utils/Android.bp
@@ -31,4 +31,7 @@
         "com.android.healthfitness",
         "com.android.permission",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
new file mode 100644
index 0000000..fd2f9bd
--- /dev/null
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.settingslib.flags"
+
+flag {
+    name: "new_status_bar_icons"
+    namespace: "systemui"
+    description: "Enable new status bar system icons"
+    bug: "314812750"
+}
+
diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp
index 202e7fe..3b14712 100644
--- a/packages/SettingsLib/search/Android.bp
+++ b/packages/SettingsLib/search/Android.bp
@@ -12,6 +12,9 @@
     visibility: ["//visibility:private"],
     srcs: ["interface-src/**/*.java"],
     host_supported: true,
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 android_library {
@@ -24,6 +27,9 @@
 
     sdk_version: "system_current",
     min_sdk_version: "21",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_plugin {
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index c51a9a0..8e5396f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -23,8 +23,6 @@
 import static com.android.settingslib.Utils.getColorAttrDefaultColor;
 
 import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -52,6 +50,8 @@
 import android.view.MenuItem;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
index 3b8f665..70ece0f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
@@ -21,7 +21,6 @@
 
 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
-import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
@@ -35,6 +34,7 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.PreferenceManager;
 import androidx.preference.PreferenceViewHolder;
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 107d5f8..c2be571 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -3,7 +3,6 @@
 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL;
 
 import android.annotation.ColorInt;
-import android.annotation.Nullable;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
@@ -23,10 +22,10 @@
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.drawable.Drawable;
-import android.hardware.usb.flags.Flags;
 import android.hardware.usb.UsbManager;
 import android.hardware.usb.UsbPort;
 import android.hardware.usb.UsbPortStatus;
+import android.hardware.usb.flags.Flags;
 import android.location.LocationManager;
 import android.media.AudioManager;
 import android.net.NetworkCapabilities;
@@ -47,6 +46,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java b/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java
index dae48db..b0db16f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java
@@ -17,7 +17,6 @@
 package com.android.settingslib.applications;
 
 import android.app.AppGlobals;
-import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -28,6 +27,8 @@
 import android.os.RemoteException;
 import android.util.IconDrawableFactory;
 
+import androidx.annotation.Nullable;
+
 import com.android.settingslib.widget.CandidateInfo;
 
 /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
index fa056e2b..416b369 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -26,9 +26,9 @@
 import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON;
 
 import android.annotation.IntDef;
-import android.annotation.Nullable;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
index 1900575..cf4d6be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
@@ -21,8 +21,6 @@
 
 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;
@@ -35,6 +33,9 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.settingslib.R;
 
 import java.lang.annotation.Retention;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java
index d8475b3..ca0cad7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java
@@ -39,23 +39,24 @@
     public static final int[] MEDIA_ROUTING_ATTRIBUTES = new int[] {
             // Stands for STRATEGY_MEDIA, including USAGE_GAME, USAGE_ASSISTANT,
             // USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, USAGE_ASSISTANCE_SONIFICATION
-            AudioAttributes.USAGE_MEDIA
-    };
-
-    public static final int[] RINGTONE_ROUTING_ATTRIBUTE = new int[] {
-            // Stands for STRATEGY_SONIFICATION, including USAGE_ALARM
-            AudioAttributes.USAGE_NOTIFICATION_RINGTONE
-    };
-
-    public static final int[] SYSTEM_SOUNDS_ROUTING_ATTRIBUTES = new int[] {
-            // Stands for STRATEGY_SONIFICATION_RESPECTFUL, including USAGE_NOTIFICATION_EVENT
-            AudioAttributes.USAGE_NOTIFICATION,
+            AudioAttributes.USAGE_MEDIA,
             // Stands for STRATEGY_ACCESSIBILITY
             AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
             // Stands for STRATEGY_DTMF
             AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
     };
 
+    public static final int[] RINGTONE_ROUTING_ATTRIBUTES = new int[] {
+            // Stands for STRATEGY_SONIFICATION, including USAGE_ALARM
+            AudioAttributes.USAGE_NOTIFICATION_RINGTONE
+    };
+
+    public static final int[] NOTIFICATION_ROUTING_ATTRIBUTES = new int[] {
+            // Stands for STRATEGY_SONIFICATION_RESPECTFUL, including USAGE_NOTIFICATION_EVENT
+            AudioAttributes.USAGE_NOTIFICATION,
+
+    };
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
             RoutingValue.AUTO,
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java
index c9512cd..8eaea0e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java
@@ -151,7 +151,9 @@
     private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) {
         boolean status = true;
         for (AudioProductStrategy strategy : strategies) {
-            status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
+            if (mAudioManager.getPreferredDeviceForStrategy(strategy) != null) {
+                status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
+            }
         }
 
         return status;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index 111a2c1..3a15b71 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -26,6 +26,7 @@
 import android.media.audiopolicy.AudioProductStrategy;
 import android.os.ParcelUuid;
 import android.provider.Settings;
+import android.util.FeatureFlagUtils;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -42,11 +43,13 @@
     private static final boolean DEBUG = BluetoothUtils.D;
 
     private final ContentResolver mContentResolver;
+    private final Context mContext;
     private final LocalBluetoothManager mBtManager;
     private final List<CachedBluetoothDevice> mCachedDevices;
     private final HearingAidAudioRoutingHelper mRoutingHelper;
     HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
             List<CachedBluetoothDevice> CachedDevices) {
+        mContext = context;
         mContentResolver = context.getContentResolver();
         mBtManager = localBtManager;
         mCachedDevices = CachedDevices;
@@ -56,6 +59,7 @@
     @VisibleForTesting
     HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
             List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) {
+        mContext = context;
         mContentResolver = context.getContentResolver();
         mBtManager = localBtManager;
         mCachedDevices = cachedDevices;
@@ -285,11 +289,13 @@
     }
 
     void onActiveDeviceChanged(CachedBluetoothDevice device) {
-        if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice(
-                BluetoothProfile.LE_AUDIO)) {
-            setAudioRoutingConfig(device);
-        } else {
-            clearAudioRoutingConfig();
+        if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
+            if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice(
+                    BluetoothProfile.LE_AUDIO)) {
+                setAudioRoutingConfig(device);
+            } else {
+                clearAudioRoutingConfig();
+            }
         }
     }
 
@@ -312,7 +318,7 @@
                 Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
                 HearingAidAudioRoutingConstants.RoutingValue.AUTO);
         final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver,
-                Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING,
+                Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
                 HearingAidAudioRoutingConstants.RoutingValue.AUTO);
 
         setPreferredDeviceRoutingStrategies(
@@ -322,10 +328,10 @@
                 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
                 hearingDeviceAttributes, mediaRoutingValue);
         setPreferredDeviceRoutingStrategies(
-                HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE,
+                HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
                 hearingDeviceAttributes, ringtoneRoutingValue);
         setPreferredDeviceRoutingStrategies(
-                HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES,
+                HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
                 hearingDeviceAttributes, systemSoundsRoutingValue);
     }
 
@@ -338,10 +344,10 @@
                 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
                 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
         setPreferredDeviceRoutingStrategies(
-                HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE,
+                HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
                 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
         setPreferredDeviceRoutingStrategies(
-                HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES,
+                HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
                 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 51164e8..57012aa 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -21,7 +21,6 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
-import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
@@ -31,6 +30,7 @@
 import android.os.Build;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
 import com.android.settingslib.R;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 49ac0f8..de21c54 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -19,7 +19,6 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
 import android.annotation.CallbackExecutor;
-import android.annotation.NonNull;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
@@ -44,6 +43,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
index 34008ac..34c60a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
@@ -20,7 +20,6 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
 import android.annotation.CallbackExecutor;
-import android.annotation.NonNull;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
@@ -33,6 +32,7 @@
 import android.os.Build;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
 import com.android.settingslib.R;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
index 3774b88..ab7a3db 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
@@ -21,7 +21,6 @@
 
 import android.annotation.CallbackExecutor;
 import android.annotation.IntRange;
-import android.annotation.NonNull;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
@@ -31,6 +30,7 @@
 import android.os.Build;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
 import java.util.ArrayList;
diff --git a/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java b/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java
index be420ac..43c6c0d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.connectivity;
 
-import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -31,6 +30,7 @@
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
index 067afa4..f847154 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
@@ -14,7 +14,6 @@
 
 package com.android.settingslib.core.instrumentation;
 
-import android.annotation.Nullable;
 import android.app.settings.SettingsEnums;
 import android.content.ComponentName;
 import android.content.Context;
@@ -24,6 +23,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import java.util.Map;
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java
index 2bd0b27..e974e70 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java
@@ -22,13 +22,13 @@
 import static androidx.lifecycle.Lifecycle.Event.ON_START;
 import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
 
-import android.annotation.Nullable;
 import android.app.Activity;
 import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.view.Menu;
 import android.view.MenuItem;
 
+import androidx.annotation.Nullable;
 import androidx.fragment.app.FragmentActivity;
 import androidx.lifecycle.LifecycleOwner;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
index bbb1ec6..0029e20 100644
--- a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.datetime;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.XmlResourceParser;
 import android.icu.text.TimeZoneFormat;
@@ -29,6 +28,7 @@
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.text.BidiFormatter;
 import androidx.core.text.TextDirectionHeuristicsCompat;
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java b/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java
index 11fae24..f07daa3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java
@@ -22,7 +22,6 @@
 
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
-import android.annotation.NonNull;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -45,6 +44,7 @@
 import android.os.Build;
 import android.os.UserHandle;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java
index 0b3a519..f329023 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java
@@ -16,11 +16,11 @@
 
 package com.android.settingslib.enterprise;
 
-import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.content.DialogInterface;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.settingslib.RestrictedLockUtils;
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
index 714accc..7e3395b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.enterprise;
 
-import android.annotation.NonNull;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -24,6 +23,7 @@
 import android.provider.Settings;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.settingslib.RestrictedLockUtils;
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java b/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java
index 17c2b02..71d5809 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.graph;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -35,6 +34,8 @@
 import android.graphics.drawable.Drawable;
 import android.util.TypedValue;
 
+import androidx.annotation.Nullable;
+
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java
index f01eb2a..65d53f3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java
@@ -16,7 +16,7 @@
 
 package com.android.settingslib.graph;
 
-import android.annotation.NonNull;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.PorterDuff;
@@ -25,6 +25,7 @@
 import android.graphics.drawable.LayerDrawable;
 import android.view.Gravity;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settingslib.R;
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
index 4d0804e..9a19f93 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -16,8 +16,6 @@
 
 import android.animation.ArgbEvaluator;
 import android.annotation.IntRange;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Canvas;
@@ -36,6 +34,9 @@
 import android.util.LayoutDirection;
 import android.util.PathParser;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java
index 1712a6b..318e3dd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java
@@ -16,8 +16,6 @@
 
 package com.android.settingslib.inputmethod;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -30,6 +28,8 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceFragment;
 import androidx.preference.PreferenceScreen;
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java
index 78ec58b..a2316435 100644
--- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java
@@ -16,8 +16,6 @@
 
 package com.android.settingslib.inputmethod;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -32,6 +30,8 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceScreen;
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java
index 4384400..5c0c979 100644
--- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java
@@ -17,7 +17,6 @@
 package com.android.settingslib.inputmethod;
 
 import android.annotation.AnyThread;
-import android.annotation.NonNull;
 import android.annotation.UiThread;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -26,6 +25,8 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.inputmethod.DirectBootAwareness;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 52b51d7..ba40a50 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -43,8 +43,6 @@
 
 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.TargetApi;
 import android.app.Notification;
 import android.bluetooth.BluetoothAdapter;
@@ -59,6 +57,8 @@
 import android.util.Log;
 
 import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
 import com.android.internal.annotations.VisibleForTesting;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
index 0be2e0e..97bbf12 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
@@ -16,8 +16,6 @@
 
 package com.android.settingslib.media;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
@@ -27,6 +25,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 
@@ -80,14 +81,17 @@
 
     @Override
     protected void transferToRoute(@NonNull MediaRoute2Info route) {
-        mRouterManager.transfer(mPackageName, route);
+        // TODO: b/279555229 - provide real user handle of a caller.
+        mRouterManager.transfer(mPackageName, route, android.os.Process.myUserHandle());
     }
 
     @Override
     protected boolean connectDeviceWithoutPackageName(@NonNull MediaDevice device) {
         final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null);
         if (info != null) {
-            mRouterManager.transfer(info, device.mRouteInfo);
+            // TODO: b/279555229 - provide real user handle and package name of a caller.
+            mRouterManager.transfer(
+                    info, device.mRouteInfo, android.os.Process.myUserHandle(), mPackageName);
             return true;
         }
         return false;
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index 80eeab5..0676ce5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -29,7 +29,6 @@
 import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
 
 import android.Manifest;
-import android.annotation.NonNull;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
@@ -40,6 +39,7 @@
 import android.media.RouteListingPreference;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settingslib.R;
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java
index 5e9ac5a..bcfdebe 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.net;
 
-import android.annotation.NonNull;
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
@@ -27,6 +26,7 @@
 import android.util.Pair;
 import android.util.Range;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.loader.content.AsyncTaskLoader;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
index 562d20d..dfa5ed1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
@@ -16,10 +16,10 @@
 
 package com.android.settingslib.notification;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.AlertDialog;
+import android.app.Flags;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -42,6 +42,8 @@
 import android.widget.ScrollView;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.PhoneWindow;
 import com.android.settingslib.R;
@@ -143,9 +145,16 @@
                                     Slog.d(TAG, "Invalid manual condition: " + tag.condition);
                                 }
                                 // always triggers priority-only dnd with chosen condition
-                                mNotificationManager.setZenMode(
-                                        Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                                        getRealConditionId(tag.condition), TAG);
+                                if (Flags.modesApi()) {
+                                    mNotificationManager.setZenMode(
+                                            Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                            getRealConditionId(tag.condition), TAG,
+                                            /* fromUser= */ true);
+                                } else {
+                                    mNotificationManager.setZenMode(
+                                            Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                            getRealConditionId(tag.condition), TAG);
+                                }
                             }
                         });
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
index 9084aa2..e83b9bc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
@@ -22,6 +22,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Drawable;
+import android.multiuser.Flags;
 import android.net.Uri;
 import android.util.Log;
 import android.widget.ImageView;
@@ -59,6 +60,9 @@
     private static final String IMAGES_DIR = "multi_user";
     private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png";
 
+    private static final String AVATAR_PICKER_ACTION = "com.android.avatarpicker"
+            + ".FULL_SCREEN_ACTIVITY";
+
     private final Activity mActivity;
     private final ActivityStarter mActivityStarter;
     private final ImageView mImageView;
@@ -105,7 +109,6 @@
                 onPhotoCropped(data.getData());
                 return true;
             }
-
         }
         return false;
     }
@@ -115,7 +118,13 @@
     }
 
     private void showAvatarPicker() {
-        Intent intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class);
+        Intent intent;
+        if (Flags.avatarSync()) {
+            intent = new Intent(AVATAR_PICKER_ACTION);
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+        } else {
+            intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class);
+        }
         intent.putExtra(AvatarPickerActivity.EXTRA_FILE_AUTHORITY, mFileAuthority);
         mActivityStarter.startActivityForResult(intent, REQUEST_CODE_PICK_AVATAR);
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java
index fbf8a2f..23b2cc2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.java
@@ -16,8 +16,6 @@
 
 package com.android.settingslib.volume;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
@@ -42,6 +40,9 @@
 import android.os.Message;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.io.PrintWriter;
 import java.util.HashMap;
 import java.util.HashSet;
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
index 303ee3c..cf45231 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
@@ -21,7 +21,6 @@
 
 import android.annotation.IntDef;
 import android.annotation.MainThread;
-import android.annotation.Nullable;
 import android.app.AppGlobals;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -58,6 +57,7 @@
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.CollectionUtils;
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java
index 98272cc..e508526 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java
@@ -15,7 +15,6 @@
  */
 package com.android.settingslib.wifi;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -32,6 +31,7 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceViewHolder;
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiRestrictionsCache.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiRestrictionsCache.java
index 7ffae40..c0ca1252 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiRestrictionsCache.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiRestrictionsCache.java
@@ -18,12 +18,12 @@
 
 import static android.os.UserManager.DISALLOW_CONFIG_WIFI;
 
-import android.annotation.NonNull;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.UserManager;
 import android.util.SparseArray;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import java.util.HashMap;
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStateWorker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStateWorker.java
index a0c2698..14967ec 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStateWorker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStateWorker.java
@@ -22,7 +22,6 @@
 import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
 
 import android.annotation.AnyThread;
-import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -34,6 +33,7 @@
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
diff --git a/packages/SettingsLib/tests/integ/Android.bp b/packages/SettingsLib/tests/integ/Android.bp
index 4b4caf5..644b72c 100644
--- a/packages/SettingsLib/tests/integ/Android.bp
+++ b/packages/SettingsLib/tests/integ/Android.bp
@@ -56,7 +56,7 @@
         "SettingsLibDeviceStateRotationLock",
         "SettingsLibSettingsSpinner",
         "SettingsLibUsageProgressBarPreference",
-        "settingslib_flags_lib",
+        "settingslib_media_flags_lib",
     ],
 
     dxflags: ["--multi-dex"],
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index 732c336..f4ddd0a 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -50,7 +50,7 @@
         "androidx.test.core",
         "androidx.core_core",
         "flag-junit",
-        "settingslib_flags_lib",
+        "settingslib_media_flags_lib",
         "testng", // TODO: remove once JUnit on Android provides assertThrows
     ],
     java_resource_dirs: ["config"],
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java
index 8b5ea30..c835244 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java
@@ -18,8 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeastOnce;
 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 static org.mockito.Mockito.when;
@@ -43,6 +45,7 @@
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -79,6 +82,8 @@
         when(mAudioDeviceInfo.getAddress()).thenReturn(TEST_DEVICE_ADDRESS);
         when(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).thenReturn(
                 new AudioDeviceInfo[]{mAudioDeviceInfo});
+        doReturn(Collections.emptyList()).when(mAudioManager).getPreferredDevicesForStrategy(
+                any(AudioProductStrategy.class));
         when(mAudioStrategy.getAudioAttributesForLegacyStreamType(
                 AudioManager.STREAM_MUSIC))
                 .thenReturn((new AudioAttributes.Builder()).build());
@@ -92,7 +97,10 @@
     }
 
     @Test
-    public void setPreferredDeviceRoutingStrategies_valueAuto_callRemoveStrategy() {
+    public void setPreferredDeviceRoutingStrategies_hadValueThenValueAuto_callRemoveStrategy() {
+        when(mAudioManager.getPreferredDeviceForStrategy(mAudioStrategy)).thenReturn(
+                mHearingDeviceAttribute);
+
         mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy),
                 mHearingDeviceAttribute,
                 HearingAidAudioRoutingConstants.RoutingValue.AUTO);
@@ -101,6 +109,17 @@
     }
 
     @Test
+    public void setPreferredDeviceRoutingStrategies_NoValueThenValueAuto_notCallRemoveStrategy() {
+        when(mAudioManager.getPreferredDeviceForStrategy(mAudioStrategy)).thenReturn(null);
+
+        mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy),
+                mHearingDeviceAttribute,
+                HearingAidAudioRoutingConstants.RoutingValue.AUTO);
+
+        verify(mAudioManager, never()).removePreferredDeviceForStrategy(mAudioStrategy);
+    }
+
+    @Test
     public void setPreferredDeviceRoutingStrategies_valueHearingDevice_callSetStrategy() {
         mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy),
                 mHearingDeviceAttribute,
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index ea10944..24fd06e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -42,6 +42,7 @@
 import android.media.AudioManager;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.os.Parcel;
+import android.util.FeatureFlagUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 
@@ -109,6 +110,7 @@
 
     @Before
     public void setUp() {
+        FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true);
         when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1);
         when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2);
         when(mDevice1.getName()).thenReturn(DEVICE_NAME_1);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java
index 5aee8cd..194a0e2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java
@@ -27,9 +27,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.NonNull;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.preference.PreferenceGroupAdapter;
 import androidx.preference.SwitchPreference;
 import androidx.recyclerview.widget.RecyclerView;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
index 1d5f1b2..819d4b3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
@@ -16,7 +16,7 @@
 
 package com.android.settingslib.enterprise;
 
-import android.annotation.Nullable;
+import androidx.annotation.Nullable;
 
 class FakeDeviceAdminStringProvider implements DeviceAdminStringProvider {
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java
index c79440e..77c46f7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/NetworkCycleDataLoaderTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.NonNull;
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
@@ -37,6 +36,8 @@
 import android.text.format.DateUtils;
 import android.util.Range;
 
+import androidx.annotation.NonNull;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java
index fae3aea..8448804 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java
@@ -16,12 +16,13 @@
 
 package com.android.settingslib.testutils.shadow;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.AttributionSource;
 import android.content.Context;
 import android.content.PermissionChecker;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
index dac8142..fde378f 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
@@ -15,12 +15,13 @@
  */
 package com.android.settingslib.testutils.shadow;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
 import android.media.RoutingSessionInfo;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index adebdcd..d5814e3 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -59,10 +59,10 @@
         // Note we statically link SettingsProviderLib to do some unit tests.  It's not accessible otherwise
         // because this test is not an instrumentation test. (because the target runs in the system process.)
         "SettingsProviderLib",
-
         "androidx.test.rules",
         "flag-junit",
         "junit",
+        "libaconfig_java_proto_lite",
         "mockito-target-minus-junit4",
         "platform-test-annotations",
         "truth",
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 2167a8a..5004f25 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -247,7 +247,7 @@
         Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
         Settings.Secure.HEARING_AID_CALL_ROUTING,
         Settings.Secure.HEARING_AID_MEDIA_ROUTING,
-        Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING,
+        Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
         Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
         Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED,
         Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index d9fe733..3027c5f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -209,6 +209,7 @@
         VALIDATORS.put(Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.POWER_BUTTON_LONG_PRESS_DURATION_MS, NONE_NEGATIVE_LONG_VALIDATOR);
         VALIDATORS.put(Global.STYLUS_EVER_USED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE, BOOLEAN_VALIDATOR);
 
         VALIDATORS.put(Global.Wearable.HAS_PAY_TOKENS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.GMS_CHECKIN_TIMEOUT_MIN, ANY_INTEGER_VALIDATOR);
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 868623f..0b0e182 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -396,7 +396,7 @@
                 new DiscreteValueValidator(new String[] {"0", "1", "2"}));
         VALIDATORS.put(Secure.HEARING_AID_MEDIA_ROUTING,
                 new DiscreteValueValidator(new String[] {"0", "1", "2"}));
-        VALIDATORS.put(Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING,
+        VALIDATORS.put(Secure.HEARING_AID_NOTIFICATION_ROUTING,
                 new DiscreteValueValidator(new String[] {"0", "1", "2"}));
         VALIDATORS.put(Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_EDGE_HAPTIC_ENABLED,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 5afcd5d..1670a70 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1866,8 +1866,8 @@
                 Settings.Secure.HEARING_AID_MEDIA_ROUTING,
                 SecureSettingsProto.Accessibility.HEARING_AID_MEDIA_ROUTING);
         dumpSetting(s, p,
-                Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING,
-                SecureSettingsProto.Accessibility.HEARING_AID_SYSTEM_SOUNDS_ROUTING);
+                Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
+                SecureSettingsProto.Accessibility.HEARING_AID_NOTIFICATION_ROUTING);
         dumpSetting(s, p,
                 Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
                 SecureSettingsProto.Accessibility.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 8f459c6..73c2e22 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -18,6 +18,9 @@
 
 import static android.os.Process.FIRST_APPLICATION_UID;
 
+import android.aconfig.Aconfig.flag_state;
+import android.aconfig.Aconfig.parsed_flag;
+import android.aconfig.Aconfig.parsed_flags;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -147,6 +150,17 @@
      */
     private static final String CONFIG_STAGED_PREFIX = "staged/";
 
+    private static final List<String> sAconfigTextProtoFilesOnDevice = List.of(
+            "/system/etc/aconfig_flags.pb",
+            "/system_ext/etc/aconfig_flags.pb",
+            "/product/etc/aconfig_flags.pb",
+            "/vendor/etc/aconfig_flags.pb");
+
+    /**
+     * This tag is applied to all aconfig default value-loaded flags.
+     */
+    private static final String BOOT_LOADED_DEFAULT_TAG = "BOOT_LOADED_DEFAULT";
+
     // This was used in version 120 and before.
     private static final String NULL_VALUE_OLD_STYLE = "null";
 
@@ -315,6 +329,59 @@
 
         synchronized (mLock) {
             readStateSyncLocked();
+
+            if (Flags.loadAconfigDefaults()) {
+                // Only load aconfig defaults if this is the first boot, the XML
+                // file doesn't exist yet, or this device is on its first boot after
+                // an OTA.
+                boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey)
+                        && (!file.exists()
+                                || mContext.getPackageManager().isDeviceUpgrading());
+                if (shouldLoadAconfigValues) {
+                    loadAconfigDefaultValuesLocked();
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void loadAconfigDefaultValuesLocked() {
+        for (String fileName : sAconfigTextProtoFilesOnDevice) {
+            try (FileInputStream inputStream = new FileInputStream(fileName)) {
+                byte[] contents = inputStream.readAllBytes();
+                loadAconfigDefaultValues(contents);
+            } catch (IOException e) {
+                Slog.e(LOG_TAG, "failed to read protobuf", e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    public void loadAconfigDefaultValues(byte[] fileContents) {
+        try {
+            parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents);
+
+            if (parsedFlags == null) {
+                Slog.e(LOG_TAG, "failed to parse aconfig protobuf");
+                return;
+            }
+
+            for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
+                String flagName = flag.getNamespace() + "/"
+                        + flag.getPackage() + "." + flag.getName();
+                String value = flag.getState() == flag_state.ENABLED ? "true" : "false";
+
+                Setting existingSetting = getSettingLocked(flagName);
+                boolean isDefaultLoaded = existingSetting.getTag() != null
+                        && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG);
+                if (existingSetting.getValue() == null || isDefaultLoaded) {
+                    insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG,
+                            false, flag.getPackage());
+                }
+            }
+        } catch (IOException e) {
+            Slog.e(LOG_TAG, "failed to parse protobuf", e);
         }
     }
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index 27ce0d4..ecac5ee 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -6,3 +6,11 @@
     description: "When enabled, allows setting and displaying local overrides via adb."
     bug: "298392357"
 }
+
+flag {
+    name: "load_aconfig_defaults"
+    namespace: "core_experiments_team_internal"
+    description: "When enabled, loads aconfig default values into DeviceConfig on boot."
+    bug: "311155098"
+    is_fixed_read_only: true
+}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 85e8769..6ad10cc 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -350,6 +350,7 @@
                     Settings.Global.DSRM_DURATION_MILLIS,
                     Settings.Global.DSRM_ENABLED_ACTIONS,
                     Settings.Global.MODE_RINGER,
+                    Settings.Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE,
                     Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION,
                     Settings.Global.MULTI_SIM_SMS_PROMPT,
                     Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 02a7bc1..24625ea 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -15,6 +15,9 @@
  */
 package com.android.providers.settings;
 
+import android.aconfig.Aconfig;
+import android.aconfig.Aconfig.parsed_flag;
+import android.aconfig.Aconfig.parsed_flags;
 import android.os.Looper;
 import android.test.AndroidTestCase;
 import android.util.Xml;
@@ -84,6 +87,86 @@
         super.tearDown();
     }
 
+    public void testLoadValidAconfigProto() {
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+        Object lock = new Object();
+        SettingsState settingsState = new SettingsState(
+                getContext(), lock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        parsed_flags flags = parsed_flags
+                .newBuilder()
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage("com.android.flags")
+                        .setName("flag1")
+                        .setNamespace("test_namespace")
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage("com.android.flags")
+                        .setName("flag2")
+                        .setNamespace("test_namespace")
+                        .setDescription("another test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.ENABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .build();
+
+        synchronized (lock) {
+            settingsState.loadAconfigDefaultValues(flags.toByteArray());
+            settingsState.persistSettingsLocked();
+        }
+        settingsState.waitForHandler();
+
+        synchronized (lock) {
+            assertEquals("false",
+                    settingsState.getSettingLocked(
+                        "test_namespace/com.android.flags.flag1").getValue());
+            assertEquals("true",
+                    settingsState.getSettingLocked(
+                        "test_namespace/com.android.flags.flag2").getValue());
+        }
+    }
+
+    public void testSkipLoadingAconfigFlagWithMissingFields() {
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+        Object lock = new Object();
+        SettingsState settingsState = new SettingsState(
+                getContext(), lock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        parsed_flags flags = parsed_flags
+                .newBuilder()
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .build();
+
+        synchronized (lock) {
+            settingsState.loadAconfigDefaultValues(flags.toByteArray());
+            settingsState.persistSettingsLocked();
+        }
+        settingsState.waitForHandler();
+
+        synchronized (lock) {
+            assertEquals(null,
+                    settingsState.getSettingLocked(
+                        "test_namespace/com.android.flags.flag1").getValue());
+        }
+    }
+
+    public void testInvalidAconfigProtoDoesNotCrash() {
+        SettingsState settingsState = getSettingStateObject();
+        settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes());
+    }
+
     public void testIsBinary() {
         assertFalse(SettingsState.isBinary(" abc 日本語"));
 
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 6e65c16..477c42e 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -688,6 +688,9 @@
     <!-- Permission required for CTS test - CtsAmbientContextDetectionServiceDeviceTest -->
     <uses-permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" />
 
+    <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases -->
+    <uses-permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE" />
+
     <!-- Permission required for CTS test - CallAudioInterceptionTest -->
     <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION" />
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f10ac1b..cc5dfc6 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -277,6 +277,7 @@
         "SystemUIPluginLib",
         "SystemUISharedLib",
         "SystemUICustomizationLib",
+        "SystemUICustomizationTestUtils",
         "SystemUI-statsd",
         "SettingsLib",
         "com_android_systemui_flags_lib",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 1a35f04..a03fa9b 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -256,6 +256,9 @@
     <!-- launcher apps -->
     <uses-permission android:name="android.permission.ACCESS_SHORTCUTS" />
 
+    <!-- Permission to start Launcher's widget picker activity. -->
+    <uses-permission android:name="android.permission.START_WIDGET_PICKER_ACTIVITY" />
+
     <uses-permission android:name="android.permission.MODIFY_THEME_OVERLAY" />
 
     <!-- accessibility -->
@@ -974,15 +977,6 @@
             android:excludeFromRecents="true"
             android:visibleToInstantApps="true"/>
 
-        <activity android:name="com.android.systemui.communal.widgets.WidgetPickerActivity"
-            android:theme="@style/Theme.EditWidgetsActivity"
-            android:excludeFromRecents="true"
-            android:autoRemoveFromRecents="true"
-            android:showOnLockScreen="true"
-            android:launchMode="singleTop"
-            android:exported="false">
-        </activity>
-
         <activity android:name="com.android.systemui.communal.widgets.EditWidgetsActivity"
             android:theme="@style/Theme.EditWidgetsActivity"
             android:excludeFromRecents="true"
diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp
index 50ed7ab..7f16ca5 100644
--- a/packages/SystemUI/aconfig/Android.bp
+++ b/packages/SystemUI/aconfig/Android.bp
@@ -23,7 +23,8 @@
     default_visibility: [
         "//visibility:override",
         "//frameworks/base/packages/SystemUI:__subpackages__",
-        "//platform_testing:__subpackages__"
+        "//frameworks/libs/systemui/tracinglib:__subpackages__",
+        "//platform_testing:__subpackages__",
     ],
 }
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index facb244..41d12dc 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -45,6 +45,13 @@
 }
 
 flag {
+    name: "notifications_improved_hun_animation"
+    namespace: "systemui"
+    description: "Adds a translateY animation, and other improvements to match the motion specs of the HUN Intro + Outro animations."
+    bug: "243302608"
+}
+
+flag {
     name: "notification_lifetime_extension_refactor"
     namespace: "systemui"
     description: "Enables moving notification lifetime extension management from SystemUI to "
@@ -160,6 +167,15 @@
 }
 
 flag {
+    name: "edge_back_gesture_handler_thread"
+    namespace: "systemui"
+    description: "Moves the EdgeBackGestureHandler window, which is used for rendering the back "
+        "arrow, to a separate thread. Previously, the EdgeBackGestureHandler window would share "
+        "the main thread with the rest of System UI."
+    bug: "304583132"
+}
+
+flag {
     name: "new_aod_transition"
     namespace: "systemui"
     description: "New LOCKSCREEN <=> AOD transition"
@@ -189,6 +205,13 @@
 }
 
 flag {
+   name: "pss_task_switcher"
+   namespace: "systemui"
+   description: "Enable the task switcher feature for partial screen sharing"
+   bug: "317208379"
+}
+
+flag {
    name: "rest_to_unlock"
    namespace: "systemui"
    description: "Require prolonged touch for fingerprint authentication"
@@ -230,3 +253,39 @@
    description: "Enable special visual and haptic effects for quick settings tiles with long-press actions"
    bug: "229856884"
 }
+
+flag {
+   name: "switch_user_on_bg"
+   namespace: "systemui"
+   description: "Does user switching on a background thread"
+   bug: "284095720"
+}
+
+flag {
+    name: "status_bar_static_inout_indicators"
+    namespace: "systemui"
+    description: "(Upstream request) Always show the network activity inout indicators and "
+        "prefer using alpha to distinguish network activity."
+    bug: "310715220"
+}
+
+flag {
+    name: "haptic_volume_slider"
+    namespace: "systemui"
+    description: "Adds haptic feedback to the volume slider."
+    bug: "316953430"
+}
+
+flag {
+    name: "screenshare_notification_hiding"
+    namespace: "systemui"
+    description: "Enable hiding of notifications during screenshare"
+    bug: "312784809"
+}
+
+flag {
+   name: "bluetooth_qs_tile_dialog_auto_on_toggle"
+   namespace: "systemui"
+   description: "Displays the auto on toggle in the bluetooth QS tile dialog"
+   bug: "316985153"
+}
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/core/Android.bp
similarity index 100%
rename from packages/SystemUI/animation/Android.bp
rename to packages/SystemUI/animation/core/Android.bp
diff --git a/packages/SystemUI/animation/AndroidManifest.xml b/packages/SystemUI/animation/core/AndroidManifest.xml
similarity index 100%
rename from packages/SystemUI/animation/AndroidManifest.xml
rename to packages/SystemUI/animation/core/AndroidManifest.xml
diff --git a/packages/SystemUI/animation/build.gradle b/packages/SystemUI/animation/core/build.gradle
similarity index 81%
rename from packages/SystemUI/animation/build.gradle
rename to packages/SystemUI/animation/core/build.gradle
index 939455f..12637f4 100644
--- a/packages/SystemUI/animation/build.gradle
+++ b/packages/SystemUI/animation/core/build.gradle
@@ -5,8 +5,8 @@
 android {
     sourceSets {
         main {
-            java.srcDirs = ["${SYS_UI_DIR}/animation/src/com/android/systemui/surfaceeffects/"]
-            manifest.srcFile "${SYS_UI_DIR}/animation/AndroidManifest.xml"
+            java.srcDirs = ["${SYS_UI_DIR}/animation/core/src/com/android/systemui/surfaceeffects/"]
+            manifest.srcFile "${SYS_UI_DIR}/animation/core/AndroidManifest.xml"
         }
     }
 
diff --git a/packages/SystemUI/animation/res/anim/launch_dialog_enter.xml b/packages/SystemUI/animation/core/res/anim/launch_dialog_enter.xml
similarity index 100%
rename from packages/SystemUI/animation/res/anim/launch_dialog_enter.xml
rename to packages/SystemUI/animation/core/res/anim/launch_dialog_enter.xml
diff --git a/packages/SystemUI/animation/res/anim/launch_dialog_exit.xml b/packages/SystemUI/animation/core/res/anim/launch_dialog_exit.xml
similarity index 100%
rename from packages/SystemUI/animation/res/anim/launch_dialog_exit.xml
rename to packages/SystemUI/animation/core/res/anim/launch_dialog_exit.xml
diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/core/res/values/ids.xml
similarity index 100%
rename from packages/SystemUI/animation/res/values/ids.xml
rename to packages/SystemUI/animation/core/res/values/ids.xml
diff --git a/packages/SystemUI/animation/res/values/styles.xml b/packages/SystemUI/animation/core/res/values/styles.xml
similarity index 100%
rename from packages/SystemUI/animation/res/values/styles.xml
rename to packages/SystemUI/animation/core/res/values/styles.xml
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/AnimationFeatureFlags.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/AnimationFeatureFlags.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/DialogLaunchAnimator.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/DialogLaunchAnimator.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/Expandable.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/Expandable.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/FontInterpolator.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/FontInterpolator.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/FontVariationUtils.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/FontVariationUtils.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/LaunchAnimator.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/LaunchAnimator.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/LaunchableView.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/LaunchableView.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ShadeInterpolation.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/ShadeInterpolation.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/ShadeInterpolation.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/ShadeInterpolation.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/TextAnimator.kt
similarity index 93%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/TextAnimator.kt
index 8dc7495..dc54240 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/core/src/com/android/systemui/animation/TextAnimator.kt
@@ -42,9 +42,9 @@
                 return baseTypeface
             }
 
-            val axes = FontVariationAxis.fromFontVariationSettings(fVar)
-                ?.toMutableList()
-                ?: mutableListOf()
+            val axes =
+                FontVariationAxis.fromFontVariationSettings(fVar)?.toMutableList()
+                    ?: mutableListOf()
             axes.removeIf { !baseTypeface.isSupportedAxes(it.getOpenTypeTagValue()) }
             if (axes.isEmpty()) {
                 return baseTypeface
@@ -206,12 +206,14 @@
      *
      * Here is an example of font runs: "fin. 終わり"
      *
+     * ```
      * Characters :    f      i      n      .      _      終     わ     り
      * Code Points: \u0066 \u0069 \u006E \u002E \u0020 \u7D42 \u308F \u308A
      * Font Runs  : <-- Roboto-Regular.ttf          --><-- NotoSans-CJK.otf -->
      *                  runStart = 0, runLength = 5        runStart = 5, runLength = 3
      * Glyph IDs  :      194        48     7      8     4367   1039   1002
      * Glyph Index:       0          1     2      3       0      1      2
+     * ```
      *
      * In this example, the "fi" is converted into ligature form, thus the single glyph ID is
      * assigned for two characters, f and i.
@@ -243,22 +245,21 @@
     /**
      * Set text style with animation.
      *
-     * By passing -1 to weight, the view preserve the current weight.
-     * By passing -1 to textSize, the view preserve the current text size.
-     * Bu passing -1 to duration, the default text animation, 1000ms, is used.
-     * By passing false to animate, the text will be updated without animation.
+     * By passing -1 to weight, the view preserve the current weight. By passing -1 to textSize, the
+     * view preserve the current text size. Bu passing -1 to duration, the default text animation,
+     * 1000ms, is used. By passing false to animate, the text will be updated without animation.
      *
      * @param fvar an optional text fontVariationSettings.
      * @param textSize an optional font size.
-     * @param colors an optional colors array that must be the same size as numLines passed to
-     *               the TextInterpolator
+     * @param colors an optional colors array that must be the same size as numLines passed to the
+     *   TextInterpolator
      * @param strokeWidth an optional paint stroke width
      * @param animate an optional boolean indicating true for showing style transition as animation,
-     *                false for immediate style transition. True by default.
+     *   false for immediate style transition. True by default.
      * @param duration an optional animation duration in milliseconds. This is ignored if animate is
-     *                 false.
+     *   false.
      * @param interpolator an optional time interpolator. If null is passed, last set interpolator
-     *                     will be used. This is ignored if animate is false.
+     *   will be used. This is ignored if animate is false.
      */
     fun setTextStyle(
         fvar: String? = "",
@@ -324,7 +325,8 @@
     }
 
     /**
-     * Set text style with animation. Similar as
+     * Set text style with animation. Similar as fun setTextStyle( fvar: String? = "", textSize:
+     * ```
      * fun setTextStyle(
      *      fvar: String? = "",
      *      textSize: Float = -1f,
@@ -336,6 +338,7 @@
      *      delay: Long = 0,
      *      onAnimationEnd: Runnable? = null
      * )
+     * ```
      *
      * @param weight an optional style value for `wght` in fontVariationSettings.
      * @param width an optional style value for `wdth` in fontVariationSettings.
@@ -356,12 +359,13 @@
         delay: Long = 0,
         onAnimationEnd: Runnable? = null
     ) {
-        val fvar = fontVariationUtils.updateFontVariation(
-            weight = weight,
-            width = width,
-            opticalSize = opticalSize,
-            roundness = roundness,
-        )
+        val fvar =
+            fontVariationUtils.updateFontVariation(
+                weight = weight,
+                width = width,
+                opticalSize = opticalSize,
+                roundness = roundness,
+            )
         setTextStyle(
             fvar = fvar,
             textSize = textSize,
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/TextInterpolator.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/TextInterpolator.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
similarity index 92%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 00d9056..a9c53d1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -196,9 +196,9 @@
          *
          * @param includeFadeIn true if the animator should also fade in the view and child views.
          * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if
-         *     [includeFadeIn] is false.
-         * @param onAnimationEnd an optional runnable that will be run once the animation
-         *    finishes successfully. Will not be run if the animation is cancelled.
+         *   [includeFadeIn] is false.
+         * @param onAnimationEnd an optional runnable that will be run once the animation finishes
+         *   successfully. Will not be run if the animation is cancelled.
          */
         @JvmOverloads
         fun animateAddition(
@@ -241,7 +241,10 @@
                 // First, fade in the container view
                 val containerDuration = duration / 6
                 createAndStartFadeInAnimator(
-                    rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator
+                    rootView,
+                    containerDuration,
+                    startDelay = 0,
+                    interpolator = fadeInInterpolator
                 )
 
                 // Then, fade in the child views
@@ -397,7 +400,7 @@
          * included by specifying [includeMargins].
          *
          * @param onAnimationEnd an optional runnable that will be run once the animation finishes
-         *    successfully. Will not be run if the animation is cancelled.
+         *   successfully. Will not be run if the animation is cancelled.
          */
         @JvmOverloads
         fun animateRemoval(
@@ -616,6 +619,7 @@
          * not newly introduced margins are included.
          *
          * Base case
+         *
          * ```
          *     1) origin=TOP
          *         x---------x    x---------x    x---------x    x---------x    x---------x
@@ -636,9 +640,11 @@
          *                                         x-----x       x-------x     |         |
          *                                                                     x---------x
          * ```
+         *
          * In case the start and end values differ in the direction of the origin, and
          * [ignorePreviousValues] is false, the previous values are used and a translation is
          * included in addition to the view expansion.
+         *
          * ```
          *     origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
          *         x
@@ -777,8 +783,7 @@
             includeMargins: Boolean = false,
         ): Map<Bound, Int> {
             val marginAdjustment =
-                if (includeMargins &&
-                    (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
+                if (includeMargins && (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
                     val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams
                     DimenHolder(
                         left = marginLp.leftMargin,
@@ -786,9 +791,9 @@
                         right = marginLp.rightMargin,
                         bottom = marginLp.bottomMargin
                     )
-            } else {
-                DimenHolder(0, 0, 0, 0)
-            }
+                } else {
+                    DimenHolder(0, 0, 0, 0)
+                }
 
             // These are the end values to use *if* this bound is part of the destination.
             val endLeft = left - marginAdjustment.left
@@ -805,60 +810,69 @@
             //  - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight.
 
             return when (destination) {
-                Hotspot.TOP -> mapOf(
-                    Bound.TOP to endTop,
-                    Bound.BOTTOM to endTop,
-                    Bound.LEFT to left,
-                    Bound.RIGHT to right,
-                )
-                Hotspot.TOP_RIGHT -> mapOf(
-                    Bound.TOP to endTop,
-                    Bound.BOTTOM to endTop,
-                    Bound.RIGHT to endRight,
-                    Bound.LEFT to endRight,
-                )
-                Hotspot.RIGHT -> mapOf(
-                    Bound.RIGHT to endRight,
-                    Bound.LEFT to endRight,
-                    Bound.TOP to top,
-                    Bound.BOTTOM to bottom,
-                )
-                Hotspot.BOTTOM_RIGHT -> mapOf(
-                    Bound.BOTTOM to endBottom,
-                    Bound.TOP to endBottom,
-                    Bound.RIGHT to endRight,
-                    Bound.LEFT to endRight,
-                )
-                Hotspot.BOTTOM -> mapOf(
-                    Bound.BOTTOM to endBottom,
-                    Bound.TOP to endBottom,
-                    Bound.LEFT to left,
-                    Bound.RIGHT to right,
-                )
-                Hotspot.BOTTOM_LEFT -> mapOf(
-                    Bound.BOTTOM to endBottom,
-                    Bound.TOP to endBottom,
-                    Bound.LEFT to endLeft,
-                    Bound.RIGHT to endLeft,
-                )
-                Hotspot.LEFT -> mapOf(
-                    Bound.LEFT to endLeft,
-                    Bound.RIGHT to endLeft,
-                    Bound.TOP to top,
-                    Bound.BOTTOM to bottom,
-                )
-                Hotspot.TOP_LEFT -> mapOf(
-                    Bound.TOP to endTop,
-                    Bound.BOTTOM to endTop,
-                    Bound.LEFT to endLeft,
-                    Bound.RIGHT to endLeft,
-                )
-                Hotspot.CENTER -> mapOf(
-                    Bound.LEFT to (endLeft + endRight) / 2,
-                    Bound.RIGHT to (endLeft + endRight) / 2,
-                    Bound.TOP to (endTop + endBottom) / 2,
-                    Bound.BOTTOM to (endTop + endBottom) / 2,
-                )
+                Hotspot.TOP ->
+                    mapOf(
+                        Bound.TOP to endTop,
+                        Bound.BOTTOM to endTop,
+                        Bound.LEFT to left,
+                        Bound.RIGHT to right,
+                    )
+                Hotspot.TOP_RIGHT ->
+                    mapOf(
+                        Bound.TOP to endTop,
+                        Bound.BOTTOM to endTop,
+                        Bound.RIGHT to endRight,
+                        Bound.LEFT to endRight,
+                    )
+                Hotspot.RIGHT ->
+                    mapOf(
+                        Bound.RIGHT to endRight,
+                        Bound.LEFT to endRight,
+                        Bound.TOP to top,
+                        Bound.BOTTOM to bottom,
+                    )
+                Hotspot.BOTTOM_RIGHT ->
+                    mapOf(
+                        Bound.BOTTOM to endBottom,
+                        Bound.TOP to endBottom,
+                        Bound.RIGHT to endRight,
+                        Bound.LEFT to endRight,
+                    )
+                Hotspot.BOTTOM ->
+                    mapOf(
+                        Bound.BOTTOM to endBottom,
+                        Bound.TOP to endBottom,
+                        Bound.LEFT to left,
+                        Bound.RIGHT to right,
+                    )
+                Hotspot.BOTTOM_LEFT ->
+                    mapOf(
+                        Bound.BOTTOM to endBottom,
+                        Bound.TOP to endBottom,
+                        Bound.LEFT to endLeft,
+                        Bound.RIGHT to endLeft,
+                    )
+                Hotspot.LEFT ->
+                    mapOf(
+                        Bound.LEFT to endLeft,
+                        Bound.RIGHT to endLeft,
+                        Bound.TOP to top,
+                        Bound.BOTTOM to bottom,
+                    )
+                Hotspot.TOP_LEFT ->
+                    mapOf(
+                        Bound.TOP to endTop,
+                        Bound.BOTTOM to endTop,
+                        Bound.LEFT to endLeft,
+                        Bound.RIGHT to endLeft,
+                    )
+                Hotspot.CENTER ->
+                    mapOf(
+                        Bound.LEFT to (endLeft + endRight) / 2,
+                        Bound.RIGHT to (endLeft + endRight) / 2,
+                        Bound.TOP to (endTop + endBottom) / 2,
+                        Bound.BOTTOM to (endTop + endBottom) / 2,
+                    )
             }
         }
 
@@ -1083,11 +1097,13 @@
             animator.startDelay = startDelay
             animator.duration = duration
             animator.interpolator = interpolator
-            animator.addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    view.setTag(R.id.tag_alpha_animator, null /* tag */)
+            animator.addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        view.setTag(R.id.tag_alpha_animator, null /* tag */)
+                    }
                 }
-            })
+            )
 
             (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel()
             view.setTag(R.id.tag_alpha_animator, animator)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewRootSync.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/ViewRootSync.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/back/BackAnimationSpec.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/back/BackAnimationSpec.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/back/BackTransformation.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/back/BackTransformation.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableImageView.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableImageView.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableImageView.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableImageView.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableLinearLayout.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableTextView.kt b/packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableTextView.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableTextView.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/animation/view/LaunchableTextView.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaders/SolidColorShader.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaders/SparkleShader.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/AnimatorExtensions.kt b/packages/SystemUI/animation/core/src/com/android/systemui/util/AnimatorExtensions.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/util/AnimatorExtensions.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/util/AnimatorExtensions.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/core/src/com/android/systemui/util/Dialog.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/util/Dialog.kt
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt b/packages/SystemUI/animation/core/src/com/android/systemui/util/Dimension.kt
similarity index 100%
rename from packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
rename to packages/SystemUI/animation/core/src/com/android/systemui/util/Dimension.kt
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index 4d53cca..cbf2496 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.KeyguardViewConfigurator
 import com.android.systemui.keyguard.qualifiers.KeyguardRootView
 import com.android.systemui.keyguard.ui.composable.LockscreenScene
+import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModule
 import com.android.systemui.scene.shared.model.Scene
 import dagger.Binds
 import dagger.Module
@@ -29,7 +30,12 @@
 import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-@Module
+@Module(
+    includes =
+        [
+            LockscreenSceneBlueprintModule::class,
+        ],
+)
 interface LockscreenSceneModule {
 
     @Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index d83f3aa..249b3e1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -74,7 +74,7 @@
 ) {
     val currentScene: SceneKey by
         viewModel.currentScene
-            .transform<CommunalSceneKey, SceneKey> { value -> value.toTransitionSceneKey() }
+            .transform { value -> emit(value.toTransitionSceneKey()) }
             .collectAsState(TransitionSceneKey.Blank)
     val sceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) }
     // Don't show hub mode UI if keyguard is present. This is important since we're in the shade,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 2a9cf0f..55fc3a2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -20,10 +20,12 @@
 import android.util.SizeF
 import android.widget.FrameLayout
 import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -31,11 +33,13 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Edit
@@ -98,7 +102,6 @@
         modifier = modifier.fillMaxSize().background(Color.White),
     ) {
         CommunalHubLazyGrid(
-            modifier = Modifier.align(Alignment.CenterStart),
             communalContent = communalContent,
             viewModel = viewModel,
             contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize),
@@ -138,21 +141,21 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun CommunalHubLazyGrid(
+private fun BoxScope.CommunalHubLazyGrid(
     communalContent: List<CommunalContentModel>,
     viewModel: BaseCommunalViewModel,
-    modifier: Modifier = Modifier,
     contentPadding: PaddingValues,
     setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
     updateDragPositionForRemove: (offset: Offset) -> Boolean,
 ) {
-    var gridModifier = modifier
+    var gridModifier = Modifier.align(Alignment.CenterStart)
     val gridState = rememberLazyGridState()
     var list = communalContent
     var dragDropState: GridDragDropState? = null
     if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
         val contentListState = rememberContentListState(communalContent, viewModel)
         list = contentListState.list
+        // for drag & drop operations within the communal hub grid
         dragDropState =
             rememberGridDragDropState(
                 gridState = gridState,
@@ -164,9 +167,22 @@
                 .fillMaxSize()
                 .dragContainer(dragDropState, beforeContentPadding(contentPadding))
                 .onGloballyPositioned { setGridCoordinates(it) }
+        // for widgets dropped from other activities
+        val dragAndDropTargetState =
+            rememberDragAndDropTargetState(
+                gridState = gridState,
+                contentListState = contentListState,
+                updateDragPositionForRemove = updateDragPositionForRemove
+            )
+
+        // A full size box in background that listens to widget drops from the picker.
+        // Since the grid has its own listener for in-grid drag events, we use a separate element
+        // for android drag events.
+        Box(Modifier.fillMaxSize().dragAndDropTarget(dragAndDropTargetState)) {}
     } else {
         gridModifier = gridModifier.height(Dimensions.GridHeight)
     }
+
     LazyHorizontalGrid(
         modifier = gridModifier,
         state = gridState,
@@ -309,12 +325,24 @@
 ) {
     when (model) {
         is CommunalContentModel.Widget -> WidgetContent(model, size, elevation, modifier)
+        is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size)
         is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
         is CommunalContentModel.Tutorial -> TutorialContent(modifier)
         is CommunalContentModel.Umo -> Umo(viewModel, modifier)
     }
 }
 
+/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */
+@Composable
+fun WidgetPlaceholderContent(size: SizeF) {
+    Card(
+        modifier = Modifier.size(Dp(size.width), Dp(size.height)),
+        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+        border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed),
+        shape = RoundedCornerShape(16.dp)
+    ) {}
+}
+
 @Composable
 private fun WidgetContent(
     model: CommunalContentModel.Widget,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
index 89c5765..979991d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -16,11 +16,10 @@
 
 package com.android.systemui.communal.ui.compose
 
+import android.content.ComponentName
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.toMutableStateList
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 
@@ -32,6 +31,7 @@
     return remember(communalContent) {
         ContentListState(
             communalContent,
+            viewModel::onAddWidget,
             viewModel::onDeleteWidget,
             viewModel::onReorderWidgets,
         )
@@ -46,30 +46,57 @@
 class ContentListState
 internal constructor(
     communalContent: List<CommunalContentModel>,
+    private val onAddWidget: (componentName: ComponentName, priority: Int) -> Unit,
     private val onDeleteWidget: (id: Int) -> Unit,
-    private val onReorderWidgets: (ids: List<Int>) -> Unit,
+    private val onReorderWidgets: (widgetIdToPriorityMap: Map<Int, Int>) -> Unit,
 ) {
-    var list by mutableStateOf(communalContent)
+    var list = communalContent.toMutableStateList()
         private set
 
     /** Move item to a new position in the list. */
     fun onMove(fromIndex: Int, toIndex: Int) {
-        list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
+        list.apply { add(toIndex, removeAt(fromIndex)) }
     }
 
     /** Remove widget from the list and the database. */
     fun onRemove(indexToRemove: Int) {
         if (list[indexToRemove] is CommunalContentModel.Widget) {
             val widget = list[indexToRemove] as CommunalContentModel.Widget
-            list = list.toMutableList().apply { removeAt(indexToRemove) }
+            list.apply { removeAt(indexToRemove) }
             onDeleteWidget(widget.appWidgetId)
         }
     }
 
-    /** Persist the new order with all the movements happened during dragging. */
-    fun onSaveList() {
-        val widgetIds: List<Int> =
-            list.filterIsInstance<CommunalContentModel.Widget>().map { it.appWidgetId }
-        onReorderWidgets(widgetIds)
+    /**
+     * Persists the new order with all the movements happened during drag operations & the new
+     * widget drop (if applicable).
+     *
+     * @param newItemComponentName name of the new widget that was dropped into the list; null if no
+     *   new widget was added.
+     * @param newItemIndex index at which the a new widget was dropped into the list; null if no new
+     *   widget was dropped.
+     */
+    fun onSaveList(newItemComponentName: ComponentName? = null, newItemIndex: Int? = null) {
+        // filters placeholder, but, maintains the indices of the widgets as if the placeholder was
+        // in the list. When persisted in DB, this leaves space for the new item (to be added) at
+        // the correct priority.
+        val widgetIdToPriorityMap: Map<Int, Int> =
+            list
+                .mapIndexedNotNull { index, item ->
+                    if (item is CommunalContentModel.Widget) {
+                        item.appWidgetId to list.size - index
+                    } else {
+                        null
+                    }
+                }
+                .toMap()
+        // reorder and then add the new widget
+        onReorderWidgets(widgetIdToPriorityMap)
+        if (newItemComponentName != null && newItemIndex != null) {
+            onAddWidget(newItemComponentName, /*priority=*/ list.size - newItemIndex)
+        }
     }
+
+    /** Returns true if the item at given index is editable. */
+    fun isItemEditable(index: Int) = list[index] is CommunalContentModel.Widget
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
new file mode 100644
index 0000000..22aa837
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -0,0 +1,274 @@
+/*
+ * 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.communal.ui.compose
+
+import android.content.ClipDescription
+import android.content.ComponentName
+import android.content.Intent
+import android.view.DragEvent
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.draganddrop.dragAndDropTarget
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draganddrop.DragAndDropEvent
+import androidx.compose.ui.draganddrop.DragAndDropTarget
+import androidx.compose.ui.draganddrop.mimeTypes
+import androidx.compose.ui.draganddrop.toAndroidDragEvent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.ui.compose.extensions.plus
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+/**
+ * Holds state associated with dragging and dropping items from other activities into the lazy grid.
+ *
+ * @see dragAndDropTarget
+ */
+@Composable
+internal fun rememberDragAndDropTargetState(
+    gridState: LazyGridState,
+    contentListState: ContentListState,
+    updateDragPositionForRemove: (offset: Offset) -> Boolean,
+): DragAndDropTargetState {
+    val scope = rememberCoroutineScope()
+    val autoScrollSpeed = remember { mutableFloatStateOf(0f) }
+    // Threshold of distance from edges that should start auto-scroll - chosen to be a narrow value
+    // that allows differentiating intention of scrolling from intention of dragging over the first
+    // visible item.
+    val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
+    val state =
+        remember(gridState, contentListState) {
+            DragAndDropTargetState(
+                state = gridState,
+                contentListState = contentListState,
+                scope = scope,
+                autoScrollSpeed = autoScrollSpeed,
+                autoScrollThreshold = autoScrollThreshold,
+                updateDragPositionForRemove = updateDragPositionForRemove,
+            )
+        }
+    LaunchedEffect(autoScrollSpeed.floatValue) {
+        if (autoScrollSpeed.floatValue != 0f) {
+            while (isActive) {
+                gridState.scrollBy(autoScrollSpeed.floatValue)
+                delay(10)
+            }
+        }
+    }
+    return state
+}
+
+/**
+ * Attaches a listener for drag and drop events from other activities.
+ *
+ * @see androidx.compose.foundation.draganddrop.dragAndDropTarget
+ * @see DragEvent
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun Modifier.dragAndDropTarget(
+    dragDropTargetState: DragAndDropTargetState,
+): Modifier {
+    val state by rememberUpdatedState(dragDropTargetState)
+
+    return this then
+        Modifier.dragAndDropTarget(
+            shouldStartDragAndDrop = accept@{ startEvent ->
+                    startEvent.mimeTypes().any { it == ClipDescription.MIMETYPE_TEXT_INTENT }
+                },
+            target =
+                object : DragAndDropTarget {
+                    override fun onStarted(event: DragAndDropEvent) {
+                        state.onStarted()
+                    }
+
+                    override fun onMoved(event: DragAndDropEvent) {
+                        state.onMoved(event)
+                    }
+
+                    override fun onDrop(event: DragAndDropEvent): Boolean {
+                        return state.onDrop(event)
+                    }
+
+                    override fun onEnded(event: DragAndDropEvent) {
+                        state.onEnded()
+                    }
+                }
+        )
+}
+
+/**
+ * Handles dropping of an item coming from a different activity (e.g. widget picker) in to the grid
+ * corresponding to the provided [LazyGridState].
+ *
+ * Adds a placeholder container to highlight the anticipated location the widget will be dropped to.
+ * When the item is held over an empty area, the placeholder appears at the end of the grid if one
+ * didn't exist already. As user moves the item over an existing item, the placeholder appears in
+ * place of that existing item. And then, the existing item is pushed over as part of re-ordering.
+ *
+ * Once item is dropped, new ordering along with the dropped item is persisted. See
+ * [ContentListState.onSaveList].
+ *
+ * Difference between this and [GridDragDropState] is that, this is used for listening to drops from
+ * other activities. [GridDragDropState] on the other hand, handles dragging of existing items in
+ * the communal hub grid.
+ */
+internal class DragAndDropTargetState(
+    private val state: LazyGridState,
+    private val contentListState: ContentListState,
+    private val scope: CoroutineScope,
+    private val autoScrollSpeed: MutableState<Float>,
+    private val autoScrollThreshold: Float,
+    private val updateDragPositionForRemove: (offset: Offset) -> Boolean,
+) {
+    /**
+     * The placeholder item that is treated as if it is being dragged across the grid. It is added
+     * to grid once drag and drop event is started and removed when event ends.
+     */
+    private var placeHolder = CommunalContentModel.WidgetPlaceholder()
+
+    private var placeHolderIndex: Int? = null
+    private var isOnRemoveButton = false
+
+    fun onStarted() {
+        // assume item will be added to the end.
+        contentListState.list.add(placeHolder)
+        placeHolderIndex = contentListState.list.size - 1
+    }
+
+    fun onMoved(event: DragAndDropEvent) {
+        val dragEvent = event.toAndroidDragEvent()
+        isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y))
+        if (!isOnRemoveButton) {
+            findTargetItem(dragEvent)?.apply {
+                var scrollIndex: Int? = null
+                var scrollOffset: Int? = null
+                if (placeHolderIndex == state.firstVisibleItemIndex) {
+                    // Save info about the first item before the move, to neutralize the automatic
+                    // keeping first item first.
+                    scrollIndex = placeHolderIndex
+                    scrollOffset = state.firstVisibleItemScrollOffset
+                }
+
+                autoScrollIfNearEdges(dragEvent)
+
+                if (contentListState.isItemEditable(this.index)) {
+                    movePlaceholderTo(this.index)
+                    placeHolderIndex = this.index
+                }
+
+                if (scrollIndex != null && scrollOffset != null) {
+                    // this is needed to neutralize automatic keeping the first item first.
+                    scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
+                }
+            }
+        }
+    }
+
+    fun onDrop(event: DragAndDropEvent): Boolean {
+        autoScrollSpeed.value = 0f
+        if (isOnRemoveButton) {
+            return false
+        }
+        return placeHolderIndex?.let { dropIndex ->
+            val componentName = event.maybeWidgetComponentName()
+            if (componentName != null) {
+                // Placeholder isn't removed yet to allow the setting the right priority for items
+                // before adding in the new item.
+                contentListState.onSaveList(
+                    newItemComponentName = componentName,
+                    newItemIndex = dropIndex
+                )
+                return@let true
+            }
+            return false
+        }
+            ?: false
+    }
+
+    fun onEnded() {
+        autoScrollSpeed.value = 0f
+        placeHolderIndex = null
+        contentListState.list.remove(placeHolder)
+        isOnRemoveButton = updateDragPositionForRemove(Offset.Zero)
+    }
+
+    private fun autoScrollIfNearEdges(dragEvent: DragEvent) {
+        val orientation = state.layoutInfo.orientation
+        val distanceFromStart =
+            if (orientation == Orientation.Horizontal) {
+                dragEvent.x
+            } else {
+                dragEvent.y
+            }
+        val distanceFromEnd =
+            if (orientation == Orientation.Horizontal) {
+                state.layoutInfo.viewportSize.width - dragEvent.x
+            } else {
+                state.layoutInfo.viewportSize.height - dragEvent.y
+            }
+        autoScrollSpeed.value =
+            when {
+                distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
+                distanceFromStart < autoScrollThreshold ->
+                    -(autoScrollThreshold - distanceFromStart)
+                else -> 0f
+            }
+    }
+
+    private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
+        state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
+            dragEvent.x.toInt() in item.offset.x..(item.offset + item.size).x &&
+                dragEvent.y.toInt() in item.offset.y..(item.offset + item.size).y
+        }
+
+    private fun movePlaceholderTo(index: Int) {
+        val currentIndex = contentListState.list.indexOf(placeHolder)
+        if (currentIndex != index) {
+            contentListState.onMove(currentIndex, index)
+        }
+    }
+
+    /**
+     * Parses and returns the component name of the widget that was dropped into the communal grid.
+     *
+     * Returns null if the drop event didn't include the widget information.
+     */
+    private fun DragAndDropEvent.maybeWidgetComponentName(): ComponentName? {
+        val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
+        return clipData
+            ?.getItemAt(0)
+            ?.intent
+            ?.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java)
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
index 5451d05..0d460aa8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -32,15 +32,13 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.toOffset
 import androidx.compose.ui.unit.toSize
 import androidx.compose.ui.zIndex
-import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.ui.compose.extensions.plus
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
@@ -112,7 +110,7 @@
             .firstOrNull { item ->
                 // grid item offset is based off grid content container so we need to deduct
                 // before content padding from the initial pointer position
-                item.isEditable &&
+                contentListState.isItemEditable(item.index) &&
                     (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x &&
                     (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y
             }
@@ -149,7 +147,7 @@
 
         val targetItem =
             state.layoutInfo.visibleItemsInfo.find { item ->
-                item.isEditable &&
+                contentListState.isItemEditable(item.index) &&
                     middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
                     middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
                     draggingItem.index != item.index
@@ -187,10 +185,6 @@
     private val LazyGridItemInfo.offsetEnd: IntOffset
         get() = this.offset + this.size
 
-    /** Whether the grid item can be dragged or be a drop target. Only widget card is editable. */
-    private val LazyGridItemInfo.isEditable: Boolean
-        get() = contentListState.list[this.index] is CommunalContentModel.Widget
-
     /** Calculate the amount dragged out of bound on both sides. Returns 0f if not overscrolled */
     private fun checkForOverscroll(startOffset: Offset, endOffset: Offset): Float {
         return when {
@@ -210,14 +204,6 @@
     }
 }
 
-private operator fun IntOffset.plus(size: IntSize): IntOffset {
-    return IntOffset(x + size.width, y + size.height)
-}
-
-private operator fun Offset.plus(size: Size): Offset {
-    return Offset(x + size.width, y + size.height)
-}
-
 fun Modifier.dragContainer(
     dragDropState: GridDragDropState,
     beforeContentPadding: ContentPaddingInPx
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/OffsetOperators.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/OffsetOperators.kt
new file mode 100644
index 0000000..b86c07e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/OffsetOperators.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.communal.ui.compose.extensions
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+/** Adds the given size to the x and y offsets in this [IntOffset] */
+operator fun IntOffset.plus(size: IntSize): IntOffset {
+    return IntOffset(x + size.width, y + size.height)
+}
+
+/** Adds the given size to the x and y offsets in this [Offset]. */
+operator fun Offset.plus(size: Size): Offset {
+    return Offset(x + size.width, y + size.height)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
new file mode 100644
index 0000000..2cb0034
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.composable
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.transitions
+import com.android.systemui.keyguard.ui.composable.blueprint.LockscreenSceneBlueprint
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import javax.inject.Inject
+
+/**
+ * Renders the content of the lockscreen.
+ *
+ * This is separate from the [LockscreenScene] because it's meant to support usage of this UI from
+ * outside the scene container framework.
+ */
+class LockscreenContent
+@Inject
+constructor(
+    private val viewModel: LockscreenContentViewModel,
+    private val blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
+) {
+
+    private val sceneKeyByBlueprint: Map<LockscreenSceneBlueprint, SceneKey> by lazy {
+        blueprints.associateWith { blueprint -> SceneKey(blueprint.id) }
+    }
+    private val sceneKeyByBlueprintId: Map<String, SceneKey> by lazy {
+        sceneKeyByBlueprint.entries.associate { (blueprint, sceneKey) -> blueprint.id to sceneKey }
+    }
+
+    @Composable
+    fun Content(
+        modifier: Modifier = Modifier,
+    ) {
+        val coroutineScope = rememberCoroutineScope()
+        val blueprintId by viewModel.blueprintId(coroutineScope).collectAsState()
+
+        // Switch smoothly between blueprints, any composable tagged with element() will be
+        // transition-animated between any two blueprints, if they both display the same element.
+        SceneTransitionLayout(
+            currentScene = checkNotNull(sceneKeyByBlueprintId[blueprintId]),
+            onChangeScene = {},
+            transitions =
+                transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } },
+            modifier = modifier,
+        ) {
+            sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) ->
+                scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 3053654..93f31ec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
+@file:OptIn(ExperimentalFoundationApi::class)
 
 package com.android.systemui.keyguard.ui.composable
 
@@ -50,14 +50,17 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.scene.shared.model.UserAction
 import com.android.systemui.scene.ui.composable.ComposableScene
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
+/** Set this to `true` to use the LockscreenContent replacement of KeyguardRootView. */
+private val UseLockscreenContent = false
+
 /** The lock screen scene shows when the device is locked. */
 @SysUISingleton
 class LockscreenScene
@@ -66,6 +69,7 @@
     @Application private val applicationScope: CoroutineScope,
     private val viewModel: LockscreenSceneViewModel,
     @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View,
+    private val lockscreenContent: Lazy<LockscreenContent>,
 ) : ComposableScene {
     override val key = SceneKey.Lockscreen
 
@@ -91,6 +95,7 @@
         LockscreenScene(
             viewProvider = viewProvider,
             viewModel = viewModel,
+            lockscreenContent = lockscreenContent,
             modifier = modifier,
         )
     }
@@ -113,6 +118,7 @@
 private fun SceneScope.LockscreenScene(
     viewProvider: () -> View,
     viewModel: LockscreenSceneViewModel,
+    lockscreenContent: Lazy<LockscreenContent>,
     modifier: Modifier = Modifier,
 ) {
     fun findSettingsMenu(): View {
@@ -133,16 +139,24 @@
             modifier = Modifier.fillMaxSize(),
         )
 
-        AndroidView(
-            factory = { _ ->
-                val keyguardRootView = viewProvider()
-                // Remove the KeyguardRootView from any parent it might already have in legacy code
-                // just in case (a view can't have two parents).
-                (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
-                keyguardRootView
-            },
-            modifier = Modifier.fillMaxSize(),
-        )
+        if (UseLockscreenContent) {
+            lockscreenContent
+                .get()
+                .Content(
+                    modifier = Modifier.fillMaxSize(),
+                )
+        } else {
+            AndroidView(
+                factory = { _ ->
+                    val keyguardRootView = viewProvider()
+                    // Remove the KeyguardRootView from any parent it might already have in legacy
+                    // code just in case (a view can't have two parents).
+                    (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
+                    keyguardRootView
+                },
+                modifier = Modifier.fillMaxSize(),
+            )
+        }
 
         val notificationStackPosition by viewModel.keyguardRoot.notificationBounds.collectAsState()
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
new file mode 100644
index 0000000..9abb50c
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.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.keyguard.ui.composable
+
+import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
+import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            CommunalBlueprintModule::class,
+            DefaultBlueprintModule::class,
+            ShortcutsBesideUdfpsBlueprintModule::class,
+            SplitShadeBlueprintModule::class,
+        ],
+)
+interface LockscreenSceneBlueprintModule
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BlueprintAlignmentLines.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BlueprintAlignmentLines.kt
new file mode 100644
index 0000000..efa8cc7
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BlueprintAlignmentLines.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.composable.blueprint
+
+import androidx.compose.ui.layout.HorizontalAlignmentLine
+import androidx.compose.ui.layout.VerticalAlignmentLine
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Encapsulates all blueprint alignment lines.
+ *
+ * These can be used to communicate alignment lines emitted by elements that the blueprint should
+ * consume and use to know how to constrain and/or place other elements in that blueprint.
+ *
+ * For more information, please see
+ * [the official documentation](https://developer.android.com/jetpack/compose/layouts/alignment-lines).
+ */
+object BlueprintAlignmentLines {
+
+    /**
+     * Encapsulates alignment lines produced by the lock icon element.
+     *
+     * Because the lock icon is also the same element as the under-display fingerprint sensor
+     * (UDFPS), blueprints should use its alignment lines to make sure that other elements on screen
+     * do not overlap with the lock icon.
+     */
+    object LockIcon {
+
+        /** The left edge of the lock icon. */
+        val Left =
+            VerticalAlignmentLine(
+                merger = { old, new ->
+                    // When two left alignment line values are provided, choose the leftmost one:
+                    min(old, new)
+                },
+            )
+
+        /** The top edge of the lock icon. */
+        val Top =
+            HorizontalAlignmentLine(
+                merger = { old, new ->
+                    // When two top alignment line values are provided, choose the topmost one:
+                    min(old, new)
+                },
+            )
+
+        /** The right edge of the lock icon. */
+        val Right =
+            VerticalAlignmentLine(
+                merger = { old, new ->
+                    // When two right alignment line values are provided, choose the rightmost one:
+                    max(old, new)
+                },
+            )
+
+        /** The bottom edge of the lock icon. */
+        val Bottom =
+            HorizontalAlignmentLine(
+                merger = { old, new ->
+                    // When two bottom alignment line values are provided, choose the bottommost
+                    // one:
+                    max(old, new)
+                },
+            )
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
new file mode 100644
index 0000000..7eddaaf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.composable.blueprint
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.compose.animation.scene.SceneScope
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/** Renders the lockscreen scene when showing the communal glanceable hub. */
+class CommunalBlueprint @Inject constructor() : LockscreenSceneBlueprint {
+
+    override val id: String = "communal"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        Box(modifier.background(Color.Black)) {
+            Text(
+                text = "TODO(b/316211368): communal blueprint",
+                color = Color.White,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+    }
+}
+
+@Module
+interface CommunalBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
new file mode 100644
index 0000000..7314453
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.composable.blueprint
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.unit.IntRect
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
+import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
+import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
+import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
+import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/**
+ * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
+ * factor).
+ */
+class DefaultBlueprint
+@Inject
+constructor(
+    private val viewModel: LockscreenContentViewModel,
+    private val statusBarSection: StatusBarSection,
+    private val clockSection: ClockSection,
+    private val smartSpaceSection: SmartSpaceSection,
+    private val notificationSection: NotificationSection,
+    private val lockSection: LockSection,
+    private val ambientIndicationSection: AmbientIndicationSection,
+    private val bottomAreaSection: BottomAreaSection,
+) : LockscreenSceneBlueprint {
+
+    override val id: String = "default"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        val isUdfpsVisible = viewModel.isUdfpsVisible
+
+        Layout(
+            content = {
+                // Constrained to above the lock icon.
+                Column(
+                    modifier = Modifier.fillMaxWidth(),
+                ) {
+                    with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+                    with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) }
+                    with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) }
+                    with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
+                    with(notificationSection) {
+                        Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+                    }
+                    if (!isUdfpsVisible) {
+                        with(ambientIndicationSection) {
+                            AmbientIndication(modifier = Modifier.fillMaxWidth())
+                        }
+                    }
+                }
+
+                with(lockSection) { LockIcon() }
+
+                // Aligned to bottom and constrained to below the lock icon.
+                Column(modifier = Modifier.fillMaxWidth()) {
+                    if (isUdfpsVisible) {
+                        with(ambientIndicationSection) {
+                            AmbientIndication(modifier = Modifier.fillMaxWidth())
+                        }
+                    }
+
+                    with(bottomAreaSection) { IndicationArea(modifier = Modifier.fillMaxWidth()) }
+                }
+
+                // Aligned to bottom and NOT constrained by the lock icon.
+                with(bottomAreaSection) {
+                    Shortcut(isStart = true, applyPadding = true)
+                    Shortcut(isStart = false, applyPadding = true)
+                }
+            },
+            modifier = modifier,
+        ) { measurables, constraints ->
+            check(measurables.size == 5)
+            val (
+                aboveLockIconMeasurable,
+                lockIconMeasurable,
+                belowLockIconMeasurable,
+                startShortcutMeasurable,
+                endShortcutMeasurable,
+            ) = measurables
+
+            val noMinConstraints =
+                constraints.copy(
+                    minWidth = 0,
+                    minHeight = 0,
+                )
+            val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
+            val lockIconBounds =
+                IntRect(
+                    left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
+                    top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
+                    right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
+                    bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
+                )
+
+            val aboveLockIconPlaceable =
+                aboveLockIconMeasurable.measure(
+                    noMinConstraints.copy(maxHeight = lockIconBounds.top)
+                )
+            val belowLockIconPlaceable =
+                belowLockIconMeasurable.measure(
+                    noMinConstraints.copy(maxHeight = constraints.maxHeight - lockIconBounds.bottom)
+                )
+            val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
+            val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints)
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                aboveLockIconPlaceable.place(
+                    x = 0,
+                    y = 0,
+                )
+                lockIconPlaceable.place(
+                    x = lockIconBounds.left,
+                    y = lockIconBounds.top,
+                )
+                belowLockIconPlaceable.place(
+                    x = 0,
+                    y = constraints.maxHeight - belowLockIconPlaceable.height,
+                )
+                startShortcutPleaceable.place(
+                    x = 0,
+                    y = constraints.maxHeight - startShortcutPleaceable.height,
+                )
+                endShortcutPleaceable.place(
+                    x = constraints.maxWidth - endShortcutPleaceable.width,
+                    y = constraints.maxHeight - endShortcutPleaceable.height,
+                )
+            }
+        }
+    }
+}
+
+@Module
+interface DefaultBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
new file mode 100644
index 0000000..6d9cba4
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.blueprint
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
+
+/** Defines interface for classes that can render the content for a specific blueprint/layout. */
+interface LockscreenSceneBlueprint {
+
+    /** The ID that uniquely identifies this blueprint across all other blueprints. */
+    val id: String
+
+    /** Renders the content of this blueprint. */
+    @Composable fun SceneScope.Content(modifier: Modifier)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
new file mode 100644
index 0000000..4c119c7
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.composable.blueprint
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.unit.IntRect
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
+import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
+import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
+import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
+import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/**
+ * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
+ * factor).
+ */
+class ShortcutsBesideUdfpsBlueprint
+@Inject
+constructor(
+    private val viewModel: LockscreenContentViewModel,
+    private val statusBarSection: StatusBarSection,
+    private val clockSection: ClockSection,
+    private val smartSpaceSection: SmartSpaceSection,
+    private val notificationSection: NotificationSection,
+    private val lockSection: LockSection,
+    private val ambientIndicationSection: AmbientIndicationSection,
+    private val bottomAreaSection: BottomAreaSection,
+) : LockscreenSceneBlueprint {
+
+    override val id: String = "shortcuts-besides-udfps"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        val isUdfpsVisible = viewModel.isUdfpsVisible
+
+        Layout(
+            content = {
+                // Constrained to above the lock icon.
+                Column(
+                    modifier = Modifier.fillMaxWidth(),
+                ) {
+                    with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+                    with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) }
+                    with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) }
+                    with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
+                    with(notificationSection) {
+                        Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+                    }
+                    if (!isUdfpsVisible) {
+                        with(ambientIndicationSection) {
+                            AmbientIndication(modifier = Modifier.fillMaxWidth())
+                        }
+                    }
+                }
+
+                // Constrained to the left of the lock icon (in left-to-right layouts).
+                with(bottomAreaSection) { Shortcut(isStart = true, applyPadding = false) }
+
+                with(lockSection) { LockIcon() }
+
+                // Constrained to the right of the lock icon (in left-to-right layouts).
+                with(bottomAreaSection) { Shortcut(isStart = false, applyPadding = false) }
+
+                // Aligned to bottom and constrained to below the lock icon.
+                Column(modifier = Modifier.fillMaxWidth()) {
+                    if (isUdfpsVisible) {
+                        with(ambientIndicationSection) {
+                            AmbientIndication(modifier = Modifier.fillMaxWidth())
+                        }
+                    }
+
+                    with(bottomAreaSection) { IndicationArea(modifier = Modifier.fillMaxWidth()) }
+                }
+            },
+            modifier = modifier,
+        ) { measurables, constraints ->
+            check(measurables.size == 5)
+            val (
+                aboveLockIconMeasurable,
+                startSideShortcutMeasurable,
+                lockIconMeasurable,
+                endSideShortcutMeasurable,
+                belowLockIconMeasurable,
+            ) = measurables
+
+            val noMinConstraints =
+                constraints.copy(
+                    minWidth = 0,
+                    minHeight = 0,
+                )
+
+            val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
+            val lockIconBounds =
+                IntRect(
+                    left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
+                    top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
+                    right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
+                    bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
+                )
+
+            val aboveLockIconPlaceable =
+                aboveLockIconMeasurable.measure(
+                    noMinConstraints.copy(maxHeight = lockIconBounds.top)
+                )
+            val startSideShortcutPlaceable = startSideShortcutMeasurable.measure(noMinConstraints)
+            val endSideShortcutPlaceable = endSideShortcutMeasurable.measure(noMinConstraints)
+            val belowLockIconPlaceable =
+                belowLockIconMeasurable.measure(
+                    noMinConstraints.copy(maxHeight = constraints.maxHeight - lockIconBounds.bottom)
+                )
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                aboveLockIconPlaceable.place(
+                    x = 0,
+                    y = 0,
+                )
+                startSideShortcutPlaceable.placeRelative(
+                    x = lockIconBounds.left / 2 - startSideShortcutPlaceable.width / 2,
+                    y = lockIconBounds.center.y - startSideShortcutPlaceable.height / 2,
+                )
+                lockIconPlaceable.place(
+                    x = lockIconBounds.left,
+                    y = lockIconBounds.top,
+                )
+                endSideShortcutPlaceable.placeRelative(
+                    x =
+                        lockIconBounds.right + (constraints.maxWidth - lockIconBounds.right) / 2 -
+                            endSideShortcutPlaceable.width / 2,
+                    y = lockIconBounds.center.y - endSideShortcutPlaceable.height / 2,
+                )
+                belowLockIconPlaceable.place(
+                    x = 0,
+                    y = constraints.maxHeight - belowLockIconPlaceable.height,
+                )
+            }
+        }
+    }
+}
+
+@Module
+interface ShortcutsBesideUdfpsBlueprintModule {
+    @Binds
+    @IntoSet
+    fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
new file mode 100644
index 0000000..7545d5f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.composable.blueprint
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.compose.animation.scene.SceneScope
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+
+/**
+ * Renders the lockscreen scene when showing with a split shade (e.g. unfolded foldable and/or
+ * tablet form factor).
+ */
+class SplitShadeBlueprint @Inject constructor() : LockscreenSceneBlueprint {
+
+    override val id: String = "split-shade"
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        Box(modifier.background(Color.Black)) {
+            Text(
+                text = "TODO(b/316211368): split shade blueprint",
+                color = Color.White,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+    }
+}
+
+@Module
+interface SplitShadeBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: SplitShadeBlueprint): LockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
new file mode 100644
index 0000000..0e7ac5e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import javax.inject.Inject
+
+class AmbientIndicationSection @Inject constructor() {
+    @Composable
+    fun SceneScope.AmbientIndication(modifier: Modifier = Modifier) {
+        MovableElement(
+            key = AmbientIndicationElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Green),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Ambient indication",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val AmbientIndicationElementKey = ElementKey("AmbientIndication")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
new file mode 100644
index 0000000..db20f65
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.composable.section
+
+import android.view.View
+import android.widget.ImageView
+import androidx.annotation.IdRes
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.res.ResourcesCompat
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.animation.view.LaunchableImageView
+import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
+import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
+import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.KeyguardIndicationController
+import com.android.systemui.statusbar.VibratorHelper
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.Flow
+
+class BottomAreaSection
+@Inject
+constructor(
+    private val viewModel: KeyguardQuickAffordancesCombinedViewModel,
+    private val falsingManager: FalsingManager,
+    private val vibratorHelper: VibratorHelper,
+    private val indicationController: KeyguardIndicationController,
+    private val indicationAreaViewModel: KeyguardIndicationAreaViewModel,
+    private val keyguardRootViewModel: KeyguardRootViewModel,
+) {
+    /**
+     * Renders a single lockscreen shortcut.
+     *
+     * @param isStart Whether the shortcut goes on the left (in left-to-right locales).
+     * @param applyPadding Whether to apply padding around the shortcut, this is needed if the
+     *   shortcut is placed along the edges of the display.
+     */
+    @Composable
+    fun SceneScope.Shortcut(
+        isStart: Boolean,
+        applyPadding: Boolean,
+        modifier: Modifier = Modifier,
+    ) {
+        MovableElement(
+            key = if (isStart) StartButtonElementKey else EndButtonElementKey,
+            modifier = modifier,
+        ) {
+            Shortcut(
+                viewId = if (isStart) R.id.start_button else R.id.end_button,
+                viewModel = if (isStart) viewModel.startButton else viewModel.endButton,
+                transitionAlpha = viewModel.transitionAlpha,
+                falsingManager = falsingManager,
+                vibratorHelper = vibratorHelper,
+                indicationController = indicationController,
+                modifier =
+                    if (applyPadding) {
+                        Modifier.shortcutPadding()
+                    } else {
+                        Modifier
+                    }
+            )
+        }
+    }
+
+    @Composable
+    fun SceneScope.IndicationArea(
+        modifier: Modifier = Modifier,
+    ) {
+        MovableElement(
+            key = IndicationAreaElementKey,
+            modifier = modifier.shortcutPadding(),
+        ) {
+            IndicationArea(
+                indicationAreaViewModel = indicationAreaViewModel,
+                keyguardRootViewModel = keyguardRootViewModel,
+                indicationController = indicationController,
+            )
+        }
+    }
+
+    @Composable
+    fun shortcutSizeDp(): DpSize {
+        return DpSize(
+            width = dimensionResource(R.dimen.keyguard_affordance_fixed_width),
+            height = dimensionResource(R.dimen.keyguard_affordance_fixed_height),
+        )
+    }
+
+    @Composable
+    private fun Shortcut(
+        @IdRes viewId: Int,
+        viewModel: Flow<KeyguardQuickAffordanceViewModel>,
+        transitionAlpha: Flow<Float>,
+        falsingManager: FalsingManager,
+        vibratorHelper: VibratorHelper,
+        indicationController: KeyguardIndicationController,
+        modifier: Modifier = Modifier,
+    ) {
+        val (binding, setBinding) = mutableStateOf<KeyguardQuickAffordanceViewBinder.Binding?>(null)
+
+        AndroidView(
+            factory = { context ->
+                val padding =
+                    context.resources.getDimensionPixelSize(
+                        R.dimen.keyguard_affordance_fixed_padding
+                    )
+                val view =
+                    LaunchableImageView(context, null).apply {
+                        id = viewId
+                        scaleType = ImageView.ScaleType.FIT_CENTER
+                        background =
+                            ResourcesCompat.getDrawable(
+                                context.resources,
+                                R.drawable.keyguard_bottom_affordance_bg,
+                                context.theme
+                            )
+                        foreground =
+                            ResourcesCompat.getDrawable(
+                                context.resources,
+                                R.drawable.keyguard_bottom_affordance_selected_border,
+                                context.theme
+                            )
+                        visibility = View.INVISIBLE
+                        setPadding(padding, padding, padding, padding)
+                    }
+
+                setBinding(
+                    KeyguardQuickAffordanceViewBinder.bind(
+                        view,
+                        viewModel,
+                        transitionAlpha,
+                        falsingManager,
+                        vibratorHelper,
+                    ) {
+                        indicationController.showTransientIndication(it)
+                    }
+                )
+
+                view
+            },
+            onRelease = { binding?.destroy() },
+            modifier =
+                modifier.size(
+                    width = shortcutSizeDp().width,
+                    height = shortcutSizeDp().height,
+                )
+        )
+    }
+
+    @Composable
+    private fun IndicationArea(
+        indicationAreaViewModel: KeyguardIndicationAreaViewModel,
+        keyguardRootViewModel: KeyguardRootViewModel,
+        indicationController: KeyguardIndicationController,
+        modifier: Modifier = Modifier,
+    ) {
+        val (disposable, setDisposable) = mutableStateOf<DisposableHandle?>(null)
+
+        AndroidView(
+            factory = { context ->
+                val view = KeyguardIndicationArea(context, null)
+                setDisposable(
+                    KeyguardIndicationAreaBinder.bind(
+                        view = view,
+                        viewModel = indicationAreaViewModel,
+                        keyguardRootViewModel = keyguardRootViewModel,
+                        indicationController = indicationController,
+                    )
+                )
+                view
+            },
+            onRelease = { disposable?.dispose() },
+            modifier = modifier.fillMaxWidth(),
+        )
+    }
+
+    @Composable
+    private fun Modifier.shortcutPadding(): Modifier {
+        return this.padding(
+                horizontal = dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+            )
+            .padding(bottom = dimensionResource(R.dimen.keyguard_affordance_vertical_offset))
+    }
+}
+
+private val StartButtonElementKey = ElementKey("StartButton")
+private val EndButtonElementKey = ElementKey("EndButton")
+private val IndicationAreaElementKey = ElementKey("IndicationArea")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
new file mode 100644
index 0000000..eaf8063
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import javax.inject.Inject
+
+class ClockSection
+@Inject
+constructor(
+    private val viewModel: KeyguardClockViewModel,
+) {
+    @Composable
+    fun SceneScope.SmallClock(modifier: Modifier = Modifier) {
+        if (viewModel.useLargeClock) {
+            return
+        }
+
+        MovableElement(
+            key = ClockElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Magenta),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Small clock",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
+        if (!viewModel.useLargeClock) {
+            return
+        }
+
+        MovableElement(
+            key = ClockElementKey,
+            modifier = modifier,
+        ) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Blue),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Large clock",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val ClockElementKey = ElementKey("Clock")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
new file mode 100644
index 0000000..2a6bea7
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.composable.section
+
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.keyguard.LockIconView
+import com.android.keyguard.LockIconViewController
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
+import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.VibratorHelper
+import dagger.Lazy
+import javax.inject.Inject
+
+class LockSection
+@Inject
+constructor(
+    private val windowManager: WindowManager,
+    private val authController: AuthController,
+    private val featureFlags: FeatureFlagsClassic,
+    private val lockIconViewController: Lazy<LockIconViewController>,
+    private val deviceEntryIconViewModel: Lazy<DeviceEntryIconViewModel>,
+    private val deviceEntryForegroundViewModel: Lazy<DeviceEntryForegroundViewModel>,
+    private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>,
+    private val falsingManager: Lazy<FalsingManager>,
+    private val vibratorHelper: Lazy<VibratorHelper>,
+) {
+    @Composable
+    fun SceneScope.LockIcon(modifier: Modifier = Modifier) {
+        if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) {
+            return
+        }
+
+        val context = LocalContext.current
+
+        AndroidView(
+            factory = { context ->
+                val view =
+                    if (DeviceEntryUdfpsRefactor.isEnabled) {
+                        DeviceEntryIconView(context, null).apply {
+                            id = R.id.device_entry_icon_view
+                            DeviceEntryIconViewBinder.bind(
+                                this,
+                                deviceEntryIconViewModel.get(),
+                                deviceEntryForegroundViewModel.get(),
+                                deviceEntryBackgroundViewModel.get(),
+                                falsingManager.get(),
+                                vibratorHelper.get(),
+                            )
+                        }
+                    } else {
+                        // keyguardBottomAreaRefactor()
+                        LockIconView(context, null).apply {
+                            id = R.id.lock_icon_view
+                            lockIconViewController.get().setLockIconView(this)
+                        }
+                    }
+                view
+            },
+            modifier =
+                modifier.element(LockIconElementKey).layout { measurable, _ ->
+                    val lockIconBounds = lockIconBounds(context)
+                    val placeable =
+                        measurable.measure(
+                            Constraints.fixed(
+                                width = lockIconBounds.width,
+                                height = lockIconBounds.height,
+                            )
+                        )
+                    layout(
+                        width = placeable.width,
+                        height = placeable.height,
+                        alignmentLines =
+                            mapOf(
+                                BlueprintAlignmentLines.LockIcon.Left to lockIconBounds.left,
+                                BlueprintAlignmentLines.LockIcon.Top to lockIconBounds.top,
+                                BlueprintAlignmentLines.LockIcon.Right to lockIconBounds.right,
+                                BlueprintAlignmentLines.LockIcon.Bottom to lockIconBounds.bottom,
+                            ),
+                    ) {
+                        placeable.place(0, 0)
+                    }
+                },
+        )
+    }
+
+    /**
+     * Returns the bounds of the lock icon, in window view coordinates.
+     *
+     * On devices that support UDFPS (under-display fingerprint sensor), the bounds of the icon are
+     * the same as the bounds of the sensor.
+     */
+    private fun lockIconBounds(
+        context: Context,
+    ): IntRect {
+        val windowViewBounds = windowManager.currentWindowMetrics.bounds
+        var widthPx = windowViewBounds.right.toFloat()
+        if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) {
+            val insets = windowManager.currentWindowMetrics.windowInsets
+            // Assumed to be initially neglected as there are no left or right insets in portrait.
+            // However, on landscape, these insets need to included when calculating the midpoint.
+            @Suppress("DEPRECATION")
+            widthPx -= (insets.systemWindowInsetLeft + insets.systemWindowInsetRight).toFloat()
+        }
+        val defaultDensity =
+            DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                DisplayMetrics.DENSITY_DEFAULT.toFloat()
+        val lockIconRadiusPx = (defaultDensity * 36).toInt()
+
+        val udfpsLocation = authController.udfpsLocation
+        val (center, radius) =
+            if (authController.isUdfpsSupported && udfpsLocation != null) {
+                Pair(
+                    IntOffset(
+                        x = udfpsLocation.x,
+                        y = udfpsLocation.y,
+                    ),
+                    authController.udfpsRadius.toInt(),
+                )
+            } else {
+                val scaleFactor = authController.scaleFactor
+                val bottomPaddingPx =
+                    context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
+                val heightPx = windowViewBounds.bottom.toFloat()
+
+                Pair(
+                    IntOffset(
+                        x = (widthPx / 2).toInt(),
+                        y =
+                            (heightPx - ((bottomPaddingPx + lockIconRadiusPx) * scaleFactor))
+                                .toInt(),
+                    ),
+                    (lockIconRadiusPx * scaleFactor).toInt(),
+                )
+            }
+
+        return IntRect(center, radius)
+    }
+}
+
+private val LockIconElementKey = ElementKey("LockIcon")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
new file mode 100644
index 0000000..ebc346d
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.composable.section
+
+import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
+import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+
+class NotificationSection
+@Inject
+constructor(
+    private val rootViewModel: KeyguardRootViewModel,
+    private val sharedNotificationContainer: SharedNotificationContainer,
+    private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
+    private val controller: NotificationStackScrollLayoutController,
+    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    @Main private val dispatcher: CoroutineDispatcher,
+    private val sceneContainerFlags: SceneContainerFlags,
+) {
+    @Composable
+    fun SceneScope.Notifications(modifier: Modifier = Modifier) {
+        MovableElement(
+            key = NotificationsElementKey,
+            modifier = modifier,
+        ) {
+            val (bounds, onBoundsChanged) = remember { mutableStateOf<Pair<Float, Float>?>(null) }
+            LaunchedEffect(bounds) {
+                bounds?.let {
+                    rootViewModel.onNotificationContainerBoundsChanged(bounds.first, bounds.second)
+                }
+            }
+
+            AndroidView(
+                factory = { context ->
+                    View(context, null).apply {
+                        id = R.id.nssl_placeholder
+                        SharedNotificationContainerBinder.bind(
+                            view = sharedNotificationContainer,
+                            viewModel = sharedNotificationContainerViewModel,
+                            sceneContainerFlags = sceneContainerFlags,
+                            controller = controller,
+                            notificationStackSizeCalculator = notificationStackSizeCalculator,
+                            mainImmediateDispatcher = dispatcher,
+                        )
+                    }
+                },
+                modifier =
+                    modifier.onPlaced {
+                        val positionInRoot = it.positionInWindow()
+                        val size = it.size
+                        onBoundsChanged(positionInRoot.y to positionInRoot.y + size.height)
+                    },
+            )
+        }
+    }
+}
+
+private val NotificationsElementKey = ElementKey("Notifications")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt
new file mode 100644
index 0000000..ca03af5
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.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.keyguard.ui.composable.section
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import javax.inject.Inject
+
+class SmartSpaceSection @Inject constructor() {
+    @Composable
+    fun SceneScope.SmartSpace(modifier: Modifier = Modifier) {
+        MovableElement(key = SmartSpaceElementKey, modifier = modifier) {
+            Box(
+                modifier = Modifier.fillMaxWidth().background(Color.Cyan),
+            ) {
+                Text(
+                    text = "TODO(b/316211368): Smart space",
+                    color = Color.White,
+                    modifier = Modifier.align(Alignment.Center),
+                )
+            }
+        }
+    }
+}
+
+private val SmartSpaceElementKey = ElementKey("SmartSpace")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
new file mode 100644
index 0000000..6811eb4
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.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.composable.section
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.height
+import com.android.keyguard.dagger.KeyguardStatusBarViewComponent
+import com.android.systemui.res.R
+import com.android.systemui.shade.NotificationPanelView
+import com.android.systemui.shade.ShadeViewStateProvider
+import com.android.systemui.statusbar.phone.KeyguardStatusBarView
+import com.android.systemui.util.Utils
+import dagger.Lazy
+import javax.inject.Inject
+
+class StatusBarSection
+@Inject
+constructor(
+    private val componentFactory: KeyguardStatusBarViewComponent.Factory,
+    private val notificationPanelView: Lazy<NotificationPanelView>,
+) {
+    @Composable
+    fun SceneScope.StatusBar(modifier: Modifier = Modifier) {
+        val context = LocalContext.current
+
+        MovableElement(
+            key = StatusBarElementKey,
+            modifier = modifier,
+        ) {
+            AndroidView(
+                factory = {
+                    notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let {
+                        (it.parent as ViewGroup).removeView(it)
+                    }
+
+                    val provider =
+                        object : ShadeViewStateProvider {
+                            override val lockscreenShadeDragProgress: Float = 0f
+                            override val panelViewExpandedHeight: Float = 0f
+                            override fun shouldHeadsUpBeVisible(): Boolean {
+                                return false
+                            }
+                        }
+
+                    @SuppressLint("InflateParams")
+                    val view =
+                        LayoutInflater.from(context)
+                            .inflate(
+                                R.layout.keyguard_status_bar,
+                                null,
+                                false,
+                            ) as KeyguardStatusBarView
+                    componentFactory.build(view, provider).keyguardStatusBarViewController.init()
+                    view
+                },
+                modifier =
+                    Modifier.fillMaxWidth().height {
+                        Utils.getStatusBarHeaderHeightKeyguard(context)
+                    },
+            )
+        }
+    }
+}
+
+private val StatusBarElementKey = ElementKey("StatusBar")
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 5dc1079..a85d9bf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import android.graphics.Picture
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
@@ -66,12 +67,21 @@
      * The movable content of this element, if this element is composed using
      * [SceneScope.MovableElement].
      */
-    val movableContent by
-        // This is only accessed from the composition (main) thread, so no need to use the default
-        // lock of lazy {} to synchronize.
-        lazy(mode = LazyThreadSafetyMode.NONE) {
-            movableContentOf { content: @Composable () -> Unit -> content() }
-        }
+    private var _movableContent: (@Composable (@Composable () -> Unit) -> Unit)? = null
+    val movableContent: @Composable (@Composable () -> Unit) -> Unit
+        get() =
+            _movableContent
+                ?: movableContentOf { content: @Composable () -> Unit -> content() }
+                    .also { _movableContent = it }
+
+    /**
+     * The [Picture] to which we save the last drawing commands of this element, if it is movable.
+     * This is necessary because the content of this element might not be composed in the scene it
+     * should currently be drawn.
+     */
+    private var _picture: Picture? = null
+    val picture: Picture
+        get() = _picture ?: Picture().also { _picture = it }
 
     override fun toString(): String {
         return "Element(key=$key)"
@@ -235,18 +245,16 @@
     }
 
     override fun ContentDrawScope.draw() {
-        if (shouldDrawElement(layoutImpl, scene, element)) {
-            val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
-            if (drawScale == Scale.Default) {
-                drawContent()
-            } else {
-                scale(
-                    drawScale.scaleX,
-                    drawScale.scaleY,
-                    if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
-                ) {
-                    this@draw.drawContent()
-                }
+        val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
+        if (drawScale == Scale.Default) {
+            drawContent()
+        } else {
+            scale(
+                drawScale.scaleX,
+                drawScale.scaleY,
+                if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
+            ) {
+                this@draw.drawContent()
             }
         }
     }
@@ -516,7 +524,7 @@
     with(placementScope) {
         // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
         // when idle.
-        val coords = coordinates!!
+        val coords = coordinates ?: error("Element ${element.key} does not have any coordinates")
         val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
         if (targetOffsetInScene != sceneValues.targetOffset) {
             // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
@@ -545,6 +553,13 @@
         lastSharedValues.offset = targetOffset
         lastValues.offset = targetOffset
 
+        // No need to place the element in this scene if we don't want to draw it anyways. Note that
+        // it's still important to compute the target offset and update lastValues, otherwise it
+        // will be out of date.
+        if (!shouldDrawElement(layoutImpl, scene, element)) {
+            return
+        }
+
         val offset = (targetOffset - currentOffset).round()
         if (isElementOpaque(layoutImpl, element, scene, sceneValues)) {
             // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 306f276..49df2f6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -16,7 +16,6 @@
 
 package com.android.compose.animation.scene
 
-import android.graphics.Picture
 import android.util.Log
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
@@ -60,7 +59,7 @@
         // The [Picture] to which we save the last drawing commands of this element. This is
         // necessary because the content of this element might not be composed in this scene, in
         // which case we still need to draw it.
-        val picture = remember { Picture() }
+        val picture = element.picture
 
         // Whether we should compose the movable element here. The scene picker logic to know in
         // which scene we should compose/draw a movable element might depend on the current
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 6a7a3a0..30e50a9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
+import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
 
 /** A scene in a [SceneTransitionLayout]. */
 @Stable
@@ -152,4 +153,8 @@
         bounds: ElementKey,
         shape: Shape
     ): Modifier = punchHole(layoutImpl, element, bounds, shape)
+
+    override fun Modifier.noResizeDuringTransitions(): Modifier {
+        return noResizeDuringTransitions(layoutState = layoutImpl.state)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 3608e37..5eb339e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -65,11 +65,11 @@
     SceneTransitionLayoutForTesting(
         currentScene,
         onChangeScene,
+        modifier,
         transitions,
         state,
         edgeDetector,
         transitionInterceptionThreshold,
-        modifier,
         onLayoutImpl = null,
         scenes,
     )
@@ -205,6 +205,12 @@
      * the result.
      */
     fun Modifier.punchHole(element: ElementKey, bounds: ElementKey, shape: Shape): Modifier
+
+    /**
+     * Don't resize during transitions. This can for instance be used to make sure that scrollable
+     * lists keep a constant size during transitions even if its elements are growing/shrinking.
+     */
+    fun Modifier.noResizeDuringTransitions(): Modifier
 }
 
 // TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey
@@ -257,12 +263,12 @@
 internal fun SceneTransitionLayoutForTesting(
     currentScene: SceneKey,
     onChangeScene: (SceneKey) -> Unit,
-    transitions: SceneTransitions,
-    state: SceneTransitionLayoutState,
-    edgeDetector: EdgeDetector,
-    transitionInterceptionThreshold: Float,
-    modifier: Modifier,
-    onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?,
+    modifier: Modifier = Modifier,
+    transitions: SceneTransitions = transitions {},
+    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+    edgeDetector: EdgeDetector = DefaultEdgeDetector,
+    transitionInterceptionThreshold: Float = 0f,
+    onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
     val density = LocalDensity.current
@@ -280,6 +286,10 @@
             .also { onLayoutImpl?.invoke(it) }
     }
 
+    // TODO(b/317014852): Move this into the SideEffect {} again once STLImpl.scenes is not a
+    // SnapshotStateMap anymore.
+    layoutImpl.updateScenes(scenes)
+
     val targetSceneChannel = remember { Channel<SceneKey>(Channel.CONFLATED) }
     SideEffect {
         if (state != layoutImpl.state) {
@@ -293,7 +303,6 @@
         (state as SceneTransitionLayoutStateImpl).transitions = transitions
         layoutImpl.density = density
         layoutImpl.edgeDetector = edgeDetector
-        layoutImpl.updateScenes(scenes)
 
         state.transitions = transitions
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index c99c325..45e1a0f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -46,7 +46,12 @@
     builder: SceneTransitionLayoutScope.() -> Unit,
     coroutineScope: CoroutineScope,
 ) {
-    internal val scenes = mutableMapOf<SceneKey, Scene>()
+    /**
+     * The map of [Scene]s.
+     *
+     * TODO(b/317014852): Make this a normal MutableMap instead.
+     */
+    internal val scenes = SnapshotStateMap<SceneKey, Scene>()
 
     /**
      * The map of [Element]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt
new file mode 100644
index 0000000..bd36cb8
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.compose.animation.scene.modifiers
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.unit.Constraints
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun Modifier.noResizeDuringTransitions(layoutState: SceneTransitionLayoutState): Modifier {
+    return intermediateLayout { measurable, constraints ->
+        if (layoutState.currentTransition == null) {
+            return@intermediateLayout measurable.measure(constraints).run {
+                layout(width, height) { place(0, 0) }
+            }
+        }
+
+        // Make sure that this layout node has the same size than when we are at rest.
+        val sizeAtRest = lookaheadSize
+        measurable.measure(Constraints.fixed(sizeAtRest.width, sizeAtRest.height)).run {
+            layout(width, height) { place(0, 0) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 439dc00..da5a0a0 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Box
@@ -30,16 +31,21 @@
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.subjects.DpOffsetSubject
+import com.android.compose.test.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -116,7 +122,13 @@
             toSceneContent = {
                 Box(Modifier.size(layoutSize)) {
                     // Shared element.
-                    Element(TestElements.Foo, elementSize, elementOffset)
+                    Element(
+                        TestElements.Foo,
+                        elementSize,
+                        elementOffset,
+                        onLayout = { fooLayouts++ },
+                        onPlacement = { fooPlacements++ },
+                    )
                 }
             },
             transition = {
@@ -127,21 +139,25 @@
                 scaleSize(TestElements.Bar, width = 1f, height = 1f)
             },
         ) {
-            var numberOfLayoutsAfterOneAnimationFrame = 0
-            var numberOfPlacementsAfterOneAnimationFrame = 0
+            var fooLayoutsAfterOneAnimationFrame = 0
+            var fooPlacementsAfterOneAnimationFrame = 0
+            var barLayoutsAfterOneAnimationFrame = 0
+            var barPlacementsAfterOneAnimationFrame = 0
 
             fun assertNumberOfLayoutsAndPlacements() {
-                assertThat(fooLayouts).isEqualTo(numberOfLayoutsAfterOneAnimationFrame)
-                assertThat(fooPlacements).isEqualTo(numberOfPlacementsAfterOneAnimationFrame)
-                assertThat(barLayouts).isEqualTo(numberOfLayoutsAfterOneAnimationFrame)
-                assertThat(barPlacements).isEqualTo(numberOfPlacementsAfterOneAnimationFrame)
+                assertThat(fooLayouts).isEqualTo(fooLayoutsAfterOneAnimationFrame)
+                assertThat(fooPlacements).isEqualTo(fooPlacementsAfterOneAnimationFrame)
+                assertThat(barLayouts).isEqualTo(barLayoutsAfterOneAnimationFrame)
+                assertThat(barPlacements).isEqualTo(barPlacementsAfterOneAnimationFrame)
             }
 
             at(16) {
                 // Capture the number of layouts and placements that happened after 1 animation
                 // frame.
-                numberOfLayoutsAfterOneAnimationFrame = fooLayouts
-                numberOfPlacementsAfterOneAnimationFrame = fooPlacements
+                fooLayoutsAfterOneAnimationFrame = fooLayouts
+                fooPlacementsAfterOneAnimationFrame = fooPlacements
+                barLayoutsAfterOneAnimationFrame = barLayouts
+                barPlacementsAfterOneAnimationFrame = barPlacements
             }
             repeat(nFrames - 2) { i ->
                 // Ensure that all animation frames (except the final one) don't relayout or replace
@@ -187,7 +203,13 @@
             toSceneContent = {
                 Box(Modifier.size(layoutSize)) {
                     // Shared element.
-                    Element(TestElements.Foo, elementSize, offset = 20.dp)
+                    Element(
+                        TestElements.Foo,
+                        elementSize,
+                        offset = 20.dp,
+                        onLayout = { fooLayouts++ },
+                        onPlacement = { fooPlacements++ },
+                    )
                 }
             },
             transition = {
@@ -198,25 +220,30 @@
                 scaleSize(TestElements.Bar, width = 1f, height = 1f)
             },
         ) {
-            var numberOfLayoutsAfterOneAnimationFrame = 0
-            var lastNumberOfPlacements = 0
+            var fooLayoutsAfterOneAnimationFrame = 0
+            var barLayoutsAfterOneAnimationFrame = 0
+            var lastFooPlacements = 0
+            var lastBarPlacements = 0
 
             fun assertNumberOfLayoutsAndPlacements() {
                 // The number of layouts have not changed.
-                assertThat(fooLayouts).isEqualTo(numberOfLayoutsAfterOneAnimationFrame)
-                assertThat(barLayouts).isEqualTo(numberOfLayoutsAfterOneAnimationFrame)
+                assertThat(fooLayouts).isEqualTo(fooLayoutsAfterOneAnimationFrame)
+                assertThat(barLayouts).isEqualTo(barLayoutsAfterOneAnimationFrame)
 
                 // The number of placements have increased.
-                assertThat(fooPlacements).isGreaterThan(lastNumberOfPlacements)
-                assertThat(barPlacements).isGreaterThan(lastNumberOfPlacements)
-                lastNumberOfPlacements = fooPlacements
+                assertThat(fooPlacements).isGreaterThan(lastFooPlacements)
+                assertThat(barPlacements).isGreaterThan(lastBarPlacements)
+                lastFooPlacements = fooPlacements
+                lastBarPlacements = barPlacements
             }
 
             at(16) {
                 // Capture the number of layouts and placements that happened after 1 animation
                 // frame.
-                numberOfLayoutsAfterOneAnimationFrame = fooLayouts
-                lastNumberOfPlacements = fooPlacements
+                fooLayoutsAfterOneAnimationFrame = fooLayouts
+                barLayoutsAfterOneAnimationFrame = barLayouts
+                lastFooPlacements = fooPlacements
+                lastBarPlacements = barPlacements
             }
             repeat(nFrames - 2) { i ->
                 // Ensure that all animation frames (except the final one) only replaced the
@@ -238,11 +265,6 @@
             SceneTransitionLayoutForTesting(
                 currentScene = currentScene,
                 onChangeScene = { currentScene = it },
-                transitions = remember { transitions {} },
-                state = remember { SceneTransitionLayoutState(currentScene) },
-                edgeDetector = DefaultEdgeDetector,
-                modifier = Modifier,
-                transitionInterceptionThreshold = 0f,
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) { /* Nothing */}
@@ -408,11 +430,6 @@
             SceneTransitionLayoutForTesting(
                 currentScene = TestScenes.SceneA,
                 onChangeScene = {},
-                transitions = remember { transitions {} },
-                state = remember { SceneTransitionLayoutState(TestScenes.SceneA) },
-                edgeDetector = DefaultEdgeDetector,
-                modifier = Modifier,
-                transitionInterceptionThreshold = 0f,
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) { Box(Modifier.element(key)) }
@@ -463,11 +480,6 @@
             SceneTransitionLayoutForTesting(
                 currentScene = TestScenes.SceneA,
                 onChangeScene = {},
-                transitions = remember { transitions {} },
-                state = remember { SceneTransitionLayoutState(TestScenes.SceneA) },
-                edgeDetector = DefaultEdgeDetector,
-                modifier = Modifier,
-                transitionInterceptionThreshold = 0f,
                 onLayoutImpl = { nullableLayoutImpl = it },
             ) {
                 scene(TestScenes.SceneA) {
@@ -553,4 +565,86 @@
             after { assertThat(fooCompositions).isEqualTo(1) }
         }
     }
+
+    @Test
+    fun sharedElementOffsetIsUpdatedEvenWhenNotPlaced() {
+        var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+        var density: Density? = null
+
+        fun layoutImpl() = nullableLayoutImpl ?: error("nullableLayoutImpl was not set")
+
+        fun density() = density ?: error("density was not set")
+
+        fun Offset.toDpOffset() = with(density()) { DpOffset(x.toDp(), y.toDp()) }
+
+        fun foo() = layoutImpl().elements[TestElements.Foo] ?: error("Foo not in elements map")
+
+        fun Element.lastSharedOffset() = lastSharedValues.offset.toDpOffset()
+
+        fun Element.lastOffsetIn(scene: SceneKey) =
+            (sceneValues[scene] ?: error("$scene not in sceneValues map"))
+                .lastValues
+                .offset
+                .toDpOffset()
+
+        rule.testTransition(
+            from = TestScenes.SceneA,
+            to = TestScenes.SceneB,
+            transitionLayout = { currentScene, onChangeScene ->
+                density = LocalDensity.current
+
+                SceneTransitionLayoutForTesting(
+                    currentScene = currentScene,
+                    onChangeScene = onChangeScene,
+                    onLayoutImpl = { nullableLayoutImpl = it },
+                    transitions =
+                        transitions {
+                            from(TestScenes.SceneA, to = TestScenes.SceneB) {
+                                spec = tween(durationMillis = 4 * 16, easing = LinearEasing)
+                            }
+                        }
+                ) {
+                    scene(TestScenes.SceneA) { Box(Modifier.element(TestElements.Foo)) }
+                    scene(TestScenes.SceneB) {
+                        Box(Modifier.offset(x = 40.dp, y = 80.dp).element(TestElements.Foo))
+                    }
+                }
+            }
+        ) {
+            val tolerance = DpOffsetSubject.DefaultTolerance
+
+            before {
+                val expected = DpOffset(0.dp, 0.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+            }
+
+            at(16) {
+                val expected = DpOffset(10.dp, 20.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+
+            at(32) {
+                val expected = DpOffset(20.dp, 40.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+
+            at(48) {
+                val expected = DpOffset(30.dp, 60.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneA)).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+
+            after {
+                val expected = DpOffset(40.dp, 80.dp)
+                assertThat(foo().lastSharedOffset()).isWithin(tolerance).of(expected)
+                assertThat(foo().lastOffsetIn(TestScenes.SceneB)).isWithin(tolerance).of(expected)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 83af630..3cd65cd 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.hasParent
 import androidx.compose.ui.test.hasText
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -92,15 +93,15 @@
             }
 
             at(32) {
-                // In the middle of the transition, there are 2 copies of the counter: the previous
-                // one from scene A (equal to 3) and the new one from scene B (equal to 0).
+                // In the middle of the transition, 2 copies of the counter are composed but only
+                // the one in scene B is placed/drawn.
                 rule
                     .onNode(
                         hasText("count: 3") and
                             hasParent(isElement(TestElements.Foo, scene = TestScenes.SceneA))
                     )
-                    .assertIsDisplayed()
-                    .assertSizeIsEqualTo(75.dp, 75.dp)
+                    .assertExists()
+                    .assertIsNotDisplayed()
 
                 rule
                     .onNode(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 321cf63..ebbd500 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -40,9 +40,7 @@
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onAllNodesWithTag
 import androidx.compose.ui.test.onChild
-import androidx.compose.ui.test.onFirst
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.unit.Dp
@@ -251,9 +249,9 @@
         // Advance to the middle of the animation.
         rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
 
-        // We need to use onAllNodesWithTag().onFirst() here given that shared elements are
-        // composed and laid out in both scenes (but drawn only in one).
-        sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.testTag).onFirst()
+        // Foo is shared between Scene A and Scene B, and is therefore placed/drawn in Scene B given
+        // that B has a higher zIndex than A.
+        sharedFoo = rule.onNode(isElement(TestElements.Foo, TestScenes.SceneB))
 
         // In scene B, foo is at the top start (x = 0, y = 0) of the layout and has a size of
         // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
@@ -287,7 +285,7 @@
         val expectedLeft = 0.dp
         val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
 
-        sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.testTag).onFirst()
+        sharedFoo = rule.onNode(isElement(TestElements.Foo, TestScenes.SceneC))
         assertThat((layoutState.transitionState as TransitionState.Transition).progress)
             .isEqualTo(interpolatedProgress)
         sharedFoo.assertWidthIsEqualTo(expectedSize)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt
new file mode 100644
index 0000000..2c159d1
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/modifiers/SizeTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.modifiers
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.element
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SizeTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun noResizeDuringTransitions() {
+        // The tag for the parent of the shared Foo element.
+        val parentTag = "parent"
+
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.element(TestElements.Foo).size(100.dp)) },
+            toSceneContent = {
+                // Don't resize the parent of Foo during transitions so that it's always the same
+                // size as when there is no transition (200dp).
+                Box(Modifier.noResizeDuringTransitions().testTag(parentTag)) {
+                    Box(Modifier.element(TestElements.Foo).size(200.dp))
+                }
+            },
+            transition = { spec = tween(durationMillis = 4 * 16, easing = LinearEasing) },
+        ) {
+            at(16) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+            at(32) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+            at(48) { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+            after { rule.onNodeWithTag(parentTag).assertSizeIsEqualTo(200.dp, 200.dp) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index e94eff3..8001f41 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.dp
@@ -33,7 +34,6 @@
 import com.android.compose.animation.scene.inScene
 import com.android.compose.animation.scene.testTransition
 import com.android.compose.test.assertSizeIsEqualTo
-import com.android.compose.test.onEach
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,28 +63,34 @@
                 onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp)
             }
             at(0) {
-                onSharedElement(TestElements.Foo).onEach {
-                    assertPositionInRootIsEqualTo(10.dp, 50.dp)
-                    assertSizeIsEqualTo(20.dp, 80.dp)
-                }
+                // Shared elements are by default placed and drawn only in the scene with highest
+                // zIndex.
+                onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
+
+                onElement(TestElements.Foo, TestScenes.SceneB)
+                    .assertPositionInRootIsEqualTo(10.dp, 50.dp)
+                    .assertSizeIsEqualTo(20.dp, 80.dp)
             }
             at(16) {
-                onSharedElement(TestElements.Foo).onEach {
-                    assertPositionInRootIsEqualTo(20.dp, 55.dp)
-                    assertSizeIsEqualTo(17.5.dp, 70.dp)
-                }
+                onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
+
+                onElement(TestElements.Foo, TestScenes.SceneB)
+                    .assertPositionInRootIsEqualTo(20.dp, 55.dp)
+                    .assertSizeIsEqualTo(17.5.dp, 70.dp)
             }
             at(32) {
-                onSharedElement(TestElements.Foo).onEach {
-                    assertPositionInRootIsEqualTo(30.dp, 60.dp)
-                    assertSizeIsEqualTo(15.dp, 60.dp)
-                }
+                onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
+
+                onElement(TestElements.Foo, TestScenes.SceneB)
+                    .assertPositionInRootIsEqualTo(30.dp, 60.dp)
+                    .assertSizeIsEqualTo(15.dp, 60.dp)
             }
             at(48) {
-                onSharedElement(TestElements.Foo).onEach {
-                    assertPositionInRootIsEqualTo(40.dp, 65.dp)
-                    assertSizeIsEqualTo(12.5.dp, 50.dp)
-                }
+                onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
+
+                onElement(TestElements.Foo, TestScenes.SceneB)
+                    .assertPositionInRootIsEqualTo(40.dp, 65.dp)
+                    .assertSizeIsEqualTo(12.5.dp, 50.dp)
             }
             after {
                 onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp)
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
new file mode 100644
index 0000000..e743c78
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.hasParent
+import androidx.compose.ui.test.hasTestTag
+
+/** A [SemanticsMatcher] that matches [element], optionally restricted to scene [scene]. */
+fun isElement(element: ElementKey, scene: SceneKey? = null): SemanticsMatcher {
+    return if (scene == null) {
+        hasTestTag(element.testTag)
+    } else {
+        hasTestTag(element.testTag) and hasParent(hasTestTag(scene.testTag))
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index 06de296..6d6d575 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -21,13 +21,8 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.SemanticsNodeInteractionCollection
-import androidx.compose.ui.test.hasParent
-import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.onAllNodesWithTag
 
 @DslMarker annotation class TransitionTestDsl
 
@@ -62,23 +57,15 @@
 
 @TransitionTestDsl
 interface TransitionTestAssertionScope {
-    fun isElement(element: ElementKey, scene: SceneKey? = null): SemanticsMatcher
-
     /**
      * Assert on [element].
      *
      * Note that presence/value assertions on the returned [SemanticsNodeInteraction] will fail if 0
      * or more than 1 elements matched [element]. If you need to assert on a shared element that
-     * will be present multiple times in the layout during transitions, either specify the [scene]
-     * in which you are matching or use [onSharedElement] instead.
+     * will be present multiple times in the layout during transitions, specify the [scene] in which
+     * you are matching.
      */
     fun onElement(element: ElementKey, scene: SceneKey? = null): SemanticsNodeInteraction
-
-    /**
-     * Assert on a shared [element]. This will throw if [element] is not shared and present only in
-     * one scene during a transition.
-     */
-    fun onSharedElement(element: ElementKey): SemanticsNodeInteractionCollection
 }
 
 /**
@@ -131,29 +118,12 @@
     val test = transitionTest(builder)
     val assertionScope =
         object : TransitionTestAssertionScope {
-            override fun isElement(element: ElementKey, scene: SceneKey?): SemanticsMatcher {
-                return if (scene == null) {
-                    hasTestTag(element.testTag)
-                } else {
-                    hasTestTag(element.testTag) and hasParent(hasTestTag(scene.testTag))
-                }
-            }
-
             override fun onElement(
                 element: ElementKey,
                 scene: SceneKey?
             ): SemanticsNodeInteraction {
                 return onNode(isElement(element, scene))
             }
-
-            override fun onSharedElement(element: ElementKey): SemanticsNodeInteractionCollection {
-                val interaction = onAllNodesWithTag(element.testTag)
-                val matches = interaction.fetchSemanticsNodes(atLeastOneRootRequired = false).size
-                if (matches < 2) {
-                    error("Element $element is not shared ($matches matches)")
-                }
-                return interaction
-            }
         }
 
     var currentScene by mutableStateOf(from)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index b9d6643..2bfa7d9 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -33,6 +33,8 @@
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.TextAnimator
 import com.android.systemui.customization.R
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.core.MessageBuffer
 import java.io.PrintWriter
@@ -51,12 +53,13 @@
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-    var messageBuffer: MessageBuffer? = null
-        set(value) {
-            logger = if (value != null) Logger(value, TAG) else null
-        }
-
-    private var logger: Logger? = null
+    // To protect us from issues from this being null while the TextView constructor is running, we
+    // implement the get method and ensure a value is returned before initialization is complete.
+    private var logger = DEFAULT_LOGGER
+        get() = field ?: DEFAULT_LOGGER
+    var messageBuffer: MessageBuffer
+        get() = logger.buffer
+        set(value) { logger = Logger(value, TAG) }
 
     private val time = Calendar.getInstance()
 
@@ -133,8 +136,8 @@
     }
 
     override fun onAttachedToWindow() {
+        logger.d("onAttachedToWindow")
         super.onAttachedToWindow()
-        logger?.d("onAttachedToWindow")
         refreshFormat()
     }
 
@@ -150,13 +153,13 @@
         time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
-        logger?.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
+        logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
         // Setting text actually triggers a layout pass (because the text view is set to
         // wrap_content width and TextView always relayouts for this). Avoid needless
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
-            logger?.d({ "refreshTime: done setting new time text to: $str1" }) {
+            logger.d({ "refreshTime: done setting new time text to: $str1" }) {
                 str1 = formattedText?.toString()
             }
             // Because the TextLayout may mutate under the hood as a result of the new text, we
@@ -165,21 +168,22 @@
             // without being notified TextInterpolator being notified.
             if (layout != null) {
                 textAnimator?.updateLayout(layout)
-                logger?.d("refreshTime: done updating textAnimator layout")
+                logger.d("refreshTime: done updating textAnimator layout")
             }
             requestLayout()
-            logger?.d("refreshTime: after requestLayout")
+            logger.d("refreshTime: after requestLayout")
         }
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
+        logger.d({ "onTimeZoneChanged($str1)" }) { str1 = timeZone?.toString() }
         time.timeZone = timeZone
         refreshFormat()
-        logger?.d({ "onTimeZoneChanged newTimeZone=$str1" }) { str1 = timeZone?.toString() }
     }
 
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        logger.d("onMeasure")
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         val animator = textAnimator
         if (animator == null) {
@@ -189,10 +193,10 @@
         } else {
             animator.updateLayout(layout)
         }
-        logger?.d("onMeasure")
     }
 
     override fun onDraw(canvas: Canvas) {
+        logger.d({ "onDraw($str1)"}) { str1 = text.toString() }
         // Use textAnimator to render text if animation is enabled.
         // Otherwise default to using standard draw functions.
         if (isAnimationEnabled) {
@@ -201,22 +205,23 @@
         } else {
             super.onDraw(canvas)
         }
-        logger?.d("onDraw")
     }
 
     override fun invalidate() {
+        @Suppress("UNNECESSARY_SAFE_CALL")
+        // logger won't be initialized when called by TextView's constructor
+        logger.d("invalidate")
         super.invalidate()
-        logger?.d("invalidate")
     }
 
     override fun onTextChanged(
-            text: CharSequence,
-            start: Int,
-            lengthBefore: Int,
-            lengthAfter: Int
+        text: CharSequence,
+        start: Int,
+        lengthBefore: Int,
+        lengthAfter: Int
     ) {
+        logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
-        logger?.d({ "onTextChanged text=$str1" }) { str1 = text.toString() }
     }
 
     fun setLineSpacingScale(scale: Float) {
@@ -230,7 +235,7 @@
     }
 
     fun animateColorChange() {
-        logger?.d("animateColorChange")
+        logger.d("animateColorChange")
         setTextStyle(
             weight = lockScreenWeight,
             textSize = -1f,
@@ -252,7 +257,7 @@
     }
 
     fun animateAppearOnLockscreen() {
-        logger?.d("animateAppearOnLockscreen")
+        logger.d("animateAppearOnLockscreen")
         setTextStyle(
             weight = dozingWeight,
             textSize = -1f,
@@ -278,7 +283,7 @@
         if (isAnimationEnabled && textAnimator == null) {
             return
         }
-        logger?.d("animateFoldAppear")
+        logger.d("animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
             textSize = -1f,
@@ -305,7 +310,7 @@
             // Skip charge animation if dozing animation is already playing.
             return
         }
-        logger?.d("animateCharge")
+        logger.d("animateCharge")
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -329,7 +334,7 @@
     }
 
     fun animateDoze(isDozing: Boolean, animate: Boolean) {
-        logger?.d("animateDoze")
+        logger.d("animateDoze")
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
             textSize = -1f,
@@ -448,7 +453,7 @@
             isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
             else -> DOUBLE_LINE_FORMAT_12_HOUR
         }
-        logger?.d({ "refreshFormat format=$str1" }) { str1 = format?.toString() }
+        logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
 
         descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
         refreshTime()
@@ -552,6 +557,8 @@
 
     companion object {
         private val TAG = AnimatableClockView::class.simpleName!!
+        private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG)
+
         const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
         private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
         private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index cdd074d..41bde52 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -21,20 +21,16 @@
 import android.net.Uri
 import android.os.UserHandle
 import android.provider.Settings
-import android.util.Log
 import androidx.annotation.OpenForTesting
-import com.android.systemui.log.LogMessageImpl
 import com.android.systemui.log.core.LogLevel
-import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
-import com.android.systemui.log.core.MessageInitializer
-import com.android.systemui.log.core.MessagePrinter
 import com.android.systemui.plugins.PluginLifecycleManager
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.plugins.PluginManager
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockId
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockMetadata
 import com.android.systemui.plugins.clocks.ClockProvider
 import com.android.systemui.plugins.clocks.ClockProviderPlugin
@@ -77,32 +73,6 @@
     return result ?: value
 }
 
-private val TMP_MESSAGE: LogMessage by lazy { LogMessageImpl.Factory.create() }
-
-private inline fun Logger?.tryLog(
-    tag: String,
-    level: LogLevel,
-    messageInitializer: MessageInitializer,
-    noinline messagePrinter: MessagePrinter,
-    ex: Throwable? = null,
-) {
-    if (this != null) {
-        // Wrap messagePrinter to convert it from crossinline to noinline
-        this.log(level, messagePrinter, ex, messageInitializer)
-    } else {
-        messageInitializer(TMP_MESSAGE)
-        val msg = messagePrinter(TMP_MESSAGE)
-        when (level) {
-            LogLevel.VERBOSE -> Log.v(tag, msg, ex)
-            LogLevel.DEBUG -> Log.d(tag, msg, ex)
-            LogLevel.INFO -> Log.i(tag, msg, ex)
-            LogLevel.WARNING -> Log.w(tag, msg, ex)
-            LogLevel.ERROR -> Log.e(tag, msg, ex)
-            LogLevel.WTF -> Log.wtf(tag, msg, ex)
-        }
-    }
-}
-
 /** ClockRegistry aggregates providers and plugins */
 open class ClockRegistry(
     val context: Context,
@@ -114,12 +84,15 @@
     val handleAllUsers: Boolean,
     defaultClockProvider: ClockProvider,
     val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
-    messageBuffer: MessageBuffer? = null,
+    val clockBuffers: ClockMessageBuffers? = null,
     val keepAllLoaded: Boolean,
     subTag: String,
     var isTransitClockEnabled: Boolean = false,
 ) {
     private val TAG = "${ClockRegistry::class.simpleName} ($subTag)"
+    private val logger: Logger =
+        Logger(clockBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG)
+
     interface ClockChangeListener {
         // Called when the active clock changes
         fun onCurrentClockChanged() {}
@@ -128,7 +101,6 @@
         fun onAvailableClocksChanged() {}
     }
 
-    private val logger: Logger? = if (messageBuffer != null) Logger(messageBuffer, TAG) else null
     private val availableClocks = ConcurrentHashMap<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
     private val settingObserver =
@@ -157,21 +129,15 @@
 
                 val knownClocks = KNOWN_PLUGINS.get(manager.getPackage())
                 if (knownClocks == null) {
-                    logger.tryLog(
-                        TAG,
-                        LogLevel.WARNING,
-                        { str1 = manager.getPackage() },
-                        { "Loading unrecognized clock package: $str1" }
-                    )
+                    logger.w({ "Loading unrecognized clock package: $str1" }) {
+                        str1 = manager.getPackage()
+                    }
                     return true
                 }
 
-                logger.tryLog(
-                    TAG,
-                    LogLevel.INFO,
-                    { str1 = manager.getPackage() },
-                    { "Skipping initial load of known clock package package: $str1" }
-                )
+                logger.i({ "Skipping initial load of known clock package package: $str1" }) {
+                    str1 = manager.getPackage()
+                }
 
                 var isCurrentClock = false
                 var isClockListChanged = false
@@ -185,19 +151,14 @@
                         }
 
                     if (manager != info.manager) {
-                        logger.tryLog(
-                            TAG,
-                            LogLevel.ERROR,
-                            {
-                                str1 = id
-                                str2 = info.manager.toString()
-                                str3 = manager.toString()
-                            },
-                            {
-                                "Clock Id conflict on attach: " +
-                                    "$str1 is double registered by $str2 and $str3"
-                            }
-                        )
+                        logger.e({
+                            "Clock Id conflict on attach: " +
+                                "$str1 is double registered by $str2 and $str3"
+                        }) {
+                            str1 = id
+                            str2 = info.manager.toString()
+                            str3 = manager.toString()
+                        }
                         continue
                     }
 
@@ -219,6 +180,8 @@
                 pluginContext: Context,
                 manager: PluginLifecycleManager<ClockProviderPlugin>
             ) {
+                plugin.initialize(clockBuffers)
+
                 var isClockListChanged = false
                 for (clock in plugin.getClocks()) {
                     val id = clock.clockId
@@ -233,19 +196,14 @@
                         }
 
                     if (manager != info.manager) {
-                        logger.tryLog(
-                            TAG,
-                            LogLevel.ERROR,
-                            {
-                                str1 = id
-                                str2 = info.manager.toString()
-                                str3 = manager.toString()
-                            },
-                            {
-                                "Clock Id conflict on load: " +
-                                    "$str1 is double registered by $str2 and $str3"
-                            }
-                        )
+                        logger.e({
+                            "Clock Id conflict on load: " +
+                                "$str1 is double registered by $str2 and $str3"
+                        }) {
+                            str1 = id
+                            str2 = info.manager.toString()
+                            str3 = manager.toString()
+                        }
                         manager.unloadPlugin()
                         continue
                     }
@@ -268,19 +226,14 @@
                     val id = clock.clockId
                     val info = availableClocks[id]
                     if (info?.manager != manager) {
-                        logger.tryLog(
-                            TAG,
-                            LogLevel.ERROR,
-                            {
-                                str1 = id
-                                str2 = info?.manager.toString()
-                                str3 = manager.toString()
-                            },
-                            {
-                                "Clock Id conflict on unload: " +
-                                    "$str1 is double registered by $str2 and $str3"
-                            }
-                        )
+                        logger.e({
+                            "Clock Id conflict on unload: " +
+                                "$str1 is double registered by $str2 and $str3"
+                        }) {
+                            str1 = id
+                            str2 = info?.manager.toString()
+                            str3 = manager.toString()
+                        }
                         continue
                     }
                     info.provider = null
@@ -350,7 +303,7 @@
 
                 ClockSettings.deserialize(json)
             } catch (ex: Exception) {
-                logger.tryLog(TAG, LogLevel.ERROR, {}, { "Failed to parse clock settings" }, ex)
+                logger.e("Failed to parse clock settings", ex)
                 null
             }
         settings = result
@@ -379,7 +332,7 @@
                 )
             }
         } catch (ex: Exception) {
-            logger.tryLog(TAG, LogLevel.ERROR, {}, { "Failed to set clock settings" }, ex)
+            logger.e("Failed to set clock settings", ex)
         }
         settings = value
     }
@@ -451,7 +404,8 @@
         }
 
     init {
-        // Register default clock designs
+        // Initialize & register default clock designs
+        defaultClockProvider.initialize(clockBuffers)
         for (clock in defaultClockProvider.getClocks()) {
             availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null)
         }
@@ -514,12 +468,7 @@
     fun verifyLoadedProviders() {
         val shouldSchedule = isQueued.compareAndSet(false, true)
         if (!shouldSchedule) {
-            logger.tryLog(
-                TAG,
-                LogLevel.VERBOSE,
-                {},
-                { "verifyLoadedProviders: shouldSchedule=false" }
-            )
+            logger.v("verifyLoadedProviders: shouldSchedule=false")
             return
         }
 
@@ -528,12 +477,7 @@
             synchronized(availableClocks) {
                 isQueued.set(false)
                 if (keepAllLoaded) {
-                    logger.tryLog(
-                        TAG,
-                        LogLevel.INFO,
-                        {},
-                        { "verifyLoadedProviders: keepAllLoaded=true" }
-                    )
+                    logger.i("verifyLoadedProviders: keepAllLoaded=true")
                     // Enforce that all plugins are loaded if requested
                     for ((_, info) in availableClocks) {
                         info.manager?.loadPlugin()
@@ -543,12 +487,7 @@
 
                 val currentClock = availableClocks[currentClockId]
                 if (currentClock == null) {
-                    logger.tryLog(
-                        TAG,
-                        LogLevel.INFO,
-                        {},
-                        { "verifyLoadedProviders: currentClock=null" }
-                    )
+                    logger.i("verifyLoadedProviders: currentClock=null")
                     // Current Clock missing, load no plugins and use default
                     for ((_, info) in availableClocks) {
                         info.manager?.unloadPlugin()
@@ -556,12 +495,7 @@
                     return@launch
                 }
 
-                logger.tryLog(
-                    TAG,
-                    LogLevel.INFO,
-                    {},
-                    { "verifyLoadedProviders: load currentClock" }
-                )
+                logger.i("verifyLoadedProviders: load currentClock")
                 val currentManager = currentClock.manager
                 currentManager?.loadPlugin()
 
@@ -577,30 +511,26 @@
 
     private fun onConnected(info: ClockInfo) {
         val isCurrent = currentClockId == info.metadata.clockId
-        logger.tryLog(
-            TAG,
+        logger.log(
             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
-            {
-                str1 = info.metadata.clockId
-                str2 = info.manager.toString()
-                bool1 = isCurrent
-            },
             { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
-        )
+        ) {
+            str1 = info.metadata.clockId
+            str2 = info.manager.toString()
+            bool1 = isCurrent
+        }
     }
 
     private fun onLoaded(info: ClockInfo) {
         val isCurrent = currentClockId == info.metadata.clockId
-        logger.tryLog(
-            TAG,
+        logger.log(
             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
-            {
-                str1 = info.metadata.clockId
-                str2 = info.manager.toString()
-                bool1 = isCurrent
-            },
             { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
-        )
+        ) {
+            str1 = info.metadata.clockId
+            str2 = info.manager.toString()
+            bool1 = isCurrent
+        }
 
         if (isCurrent) {
             triggerOnCurrentClockChanged()
@@ -609,16 +539,14 @@
 
     private fun onUnloaded(info: ClockInfo) {
         val isCurrent = currentClockId == info.metadata.clockId
-        logger.tryLog(
-            TAG,
+        logger.log(
             if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG,
-            {
-                str1 = info.metadata.clockId
-                str2 = info.manager.toString()
-                bool1 = isCurrent
-            },
             { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
-        )
+        ) {
+            str1 = info.metadata.clockId
+            str2 = info.manager.toString()
+            bool1 = isCurrent
+        }
 
         if (isCurrent) {
             triggerOnCurrentClockChanged()
@@ -627,16 +555,14 @@
 
     private fun onDisconnected(info: ClockInfo) {
         val isCurrent = currentClockId == info.metadata.clockId
-        logger.tryLog(
-            TAG,
+        logger.log(
             if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
-            {
-                str1 = info.metadata.clockId
-                str2 = info.manager.toString()
-                bool1 = isCurrent
-            },
             { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
-        )
+        ) {
+            str1 = info.metadata.clockId
+            str2 = info.manager.toString()
+            bool1 = isCurrent
+        }
     }
 
     fun getClocks(): List<ClockMetadata> {
@@ -676,23 +602,13 @@
         if (isEnabled && clockId.isNotEmpty()) {
             val clock = createClock(clockId)
             if (clock != null) {
-                logger.tryLog(TAG, LogLevel.INFO, { str1 = clockId }, { "Rendering clock $str1" })
+                logger.i({ "Rendering clock $str1" }) { str1 = clockId }
                 return clock
             } else if (availableClocks.containsKey(clockId)) {
-                logger.tryLog(
-                    TAG,
-                    LogLevel.WARNING,
-                    { str1 = clockId },
-                    { "Clock $str1 not loaded; using default" }
-                )
+                logger.w({ "Clock $str1 not loaded; using default" }) { str1 = clockId }
                 verifyLoadedProviders()
             } else {
-                logger.tryLog(
-                    TAG,
-                    LogLevel.ERROR,
-                    { str1 = clockId },
-                    { "Clock $str1 not found; using default" }
-                )
+                logger.e({ "Clock $str1 not found; using default" }) { str1 = clockId }
             }
         }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 01c03b1..99d3216 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.plugins.clocks.ClockFaceConfig
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockSettings
 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
 import com.android.systemui.plugins.clocks.WeatherData
@@ -41,8 +42,6 @@
 import java.util.Locale
 import java.util.TimeZone
 
-private val TAG = DefaultClockController::class.simpleName
-
 /**
  * Controls the default clock visuals.
  *
@@ -56,6 +55,7 @@
     private val settings: ClockSettings?,
     private val hasStepClockAnimation: Boolean = false,
     private val migratedClocks: Boolean = false,
+    messageBuffers: ClockMessageBuffers? = null,
 ) : ClockController {
     override val smallClock: DefaultClockFaceController
     override val largeClock: LargeClockFaceController
@@ -83,13 +83,15 @@
             DefaultClockFaceController(
                 layoutInflater.inflate(R.layout.clock_default_small, parent, false)
                     as AnimatableClockView,
-                settings?.seedColor
+                settings?.seedColor,
+                messageBuffers?.smallClockMessageBuffer
             )
         largeClock =
             LargeClockFaceController(
                 layoutInflater.inflate(R.layout.clock_default_large, parent, false)
                     as AnimatableClockView,
-                settings?.seedColor
+                settings?.seedColor,
+                messageBuffers?.largeClockMessageBuffer
             )
         clocks = listOf(smallClock.view, largeClock.view)
 
@@ -110,6 +112,7 @@
     open inner class DefaultClockFaceController(
         override val view: AnimatableClockView,
         var seedColor: Int?,
+        messageBuffer: MessageBuffer?,
     ) : ClockFaceController {
 
         // MAGENTA is a placeholder, and will be assigned correctly in initialize
@@ -120,12 +123,6 @@
         override val config = ClockFaceConfig()
         override val layout = DefaultClockFaceLayout(view)
 
-        override var messageBuffer: MessageBuffer?
-            get() = view.messageBuffer
-            set(value) {
-                view.messageBuffer = value
-            }
-
         override var animations: DefaultClockAnimations = DefaultClockAnimations(view, 0f, 0f)
             internal set
 
@@ -134,6 +131,7 @@
                 currentColor = seedColor!!
             }
             view.setColors(DOZE_COLOR, currentColor)
+            messageBuffer?.let { view.messageBuffer = it }
         }
 
         override val events =
@@ -188,7 +186,8 @@
     inner class LargeClockFaceController(
         view: AnimatableClockView,
         seedColor: Int?,
-    ) : DefaultClockFaceController(view, seedColor) {
+        messageBuffer: MessageBuffer?,
+    ) : DefaultClockFaceController(view, seedColor, messageBuffer) {
         override val layout = DefaultClockFaceLayout(view)
         override val config =
             ClockFaceConfig(hasCustomPositionUpdatedAnimation = hasStepClockAnimation)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index a219be5..20f87a0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.customization.R
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockId
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockMetadata
 import com.android.systemui.plugins.clocks.ClockProvider
 import com.android.systemui.plugins.clocks.ClockSettings
@@ -35,6 +36,12 @@
     val hasStepClockAnimation: Boolean = false,
     val migratedClocks: Boolean = false
 ) : ClockProvider {
+    private var messageBuffers: ClockMessageBuffers? = null
+
+    override fun initialize(buffers: ClockMessageBuffers?) {
+        messageBuffers = buffers
+    }
+
     override fun getClocks(): List<ClockMetadata> = listOf(ClockMetadata(DEFAULT_CLOCK_ID))
 
     override fun createClock(settings: ClockSettings): ClockController {
@@ -49,6 +56,7 @@
             settings,
             hasStepClockAnimation,
             migratedClocks,
+            messageBuffers,
         )
     }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
new file mode 100644
index 0000000..4b21105
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.notifications.data.repository
+
+import android.provider.Settings
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/** Provides access to state related to notification settings. */
+class NotificationSettingsRepository(
+    scope: CoroutineScope,
+    private val backgroundDispatcher: CoroutineDispatcher,
+    private val secureSettingsRepository: SecureSettingsRepository,
+) {
+    /** The current state of the notification setting. */
+    val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
+        secureSettingsRepository
+            .intSetting(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+            )
+            .map { it == 1 }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
+
+    suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
+        withContext(backgroundDispatcher) {
+            secureSettingsRepository.setInt(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = if (enabled) 1 else 0,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
new file mode 100644
index 0000000..9ec6ec8
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.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.shared.notifications.domain.interactor
+
+import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import kotlinx.coroutines.flow.StateFlow
+
+/** Encapsulates business logic for interacting with notification settings. */
+class NotificationSettingsInteractor(
+    private val repository: NotificationSettingsRepository,
+) {
+    /** Should notifications be visible on the lockscreen? */
+    val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
+        repository.isShowNotificationsOnLockScreenEnabled
+
+    suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
+        repository.setShowNotificationsOnLockscreenEnabled(enabled)
+    }
+
+    /** Toggles the setting to show or hide notifications on the lock screen. */
+    suspend fun toggleShowNotificationsOnLockscreenEnabled() {
+        val current = repository.isShowNotificationsOnLockScreenEnabled.value
+        repository.setShowNotificationsOnLockscreenEnabled(!current)
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
new file mode 100644
index 0000000..754d5dc
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.shared.settings.data.repository
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.provider.Settings
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/** Defines interface for classes that can provide access to data from [Settings.Secure]. */
+interface SecureSettingsRepository {
+
+    /** Returns a [Flow] tracking the value of a setting as an [Int]. */
+    fun intSetting(
+        name: String,
+        defaultValue: Int = 0,
+    ): Flow<Int>
+
+    /** Updates the value of the setting with the given name. */
+    suspend fun setInt(
+        name: String,
+        value: Int,
+    )
+
+    suspend fun getInt(
+        name: String,
+        defaultValue: Int = 0,
+    ): Int
+
+    suspend fun getString(name: String): String?
+}
+
+class SecureSettingsRepositoryImpl(
+    private val contentResolver: ContentResolver,
+    private val backgroundDispatcher: CoroutineDispatcher,
+) : SecureSettingsRepository {
+
+    override fun intSetting(
+        name: String,
+        defaultValue: Int,
+    ): Flow<Int> {
+        return callbackFlow {
+                val observer =
+                    object : ContentObserver(null) {
+                        override fun onChange(selfChange: Boolean) {
+                            trySend(Unit)
+                        }
+                    }
+
+                contentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(name),
+                    /* notifyForDescendants= */ false,
+                    observer,
+                )
+                send(Unit)
+
+                awaitClose { contentResolver.unregisterContentObserver(observer) }
+            }
+            .map { Settings.Secure.getInt(contentResolver, name, defaultValue) }
+            // The above work is done on the background thread (which is important for accessing
+            // settings through the content resolver).
+            .flowOn(backgroundDispatcher)
+    }
+
+    override suspend fun setInt(name: String, value: Int) {
+        withContext(backgroundDispatcher) {
+            Settings.Secure.putInt(
+                contentResolver,
+                name,
+                value,
+            )
+        }
+    }
+
+    override suspend fun getInt(name: String, defaultValue: Int): Int {
+        return withContext(backgroundDispatcher) {
+            Settings.Secure.getInt(
+                contentResolver,
+                name,
+                defaultValue,
+            )
+        }
+    }
+
+    override suspend fun getString(name: String): String? {
+        return withContext(backgroundDispatcher) {
+            Settings.Secure.getString(
+                contentResolver,
+                name,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/tests/utils/Android.bp b/packages/SystemUI/customization/tests/utils/Android.bp
new file mode 100644
index 0000000..6db1410
--- /dev/null
+++ b/packages/SystemUI/customization/tests/utils/Android.bp
@@ -0,0 +1,33 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+java_library {
+    name: "SystemUICustomizationTestUtils",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "SystemUICustomizationLib",
+    ],
+}
diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt
new file mode 100644
index 0000000..37b9792
--- /dev/null
+++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.settings.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+class FakeSecureSettingsRepository : SecureSettingsRepository {
+
+    private val settings = MutableStateFlow<Map<String, String>>(mutableMapOf())
+
+    override fun intSetting(name: String, defaultValue: Int): Flow<Int> {
+        return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() }
+    }
+
+    override suspend fun setInt(name: String, value: Int) {
+        settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() }
+    }
+
+    override suspend fun getInt(name: String, defaultValue: Int): Int {
+        return settings.value[name]?.toInt() ?: defaultValue
+    }
+
+    override suspend fun getString(name: String): String? {
+        return settings.value[name]
+    }
+}
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.kt b/packages/SystemUI/log/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.kt
new file mode 100644
index 0000000..006b521
--- /dev/null
+++ b/packages/SystemUI/log/src/com/android/systemui/log/core/LogcatOnlyMessageBuffer.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.log.core
+
+import android.util.Log
+import com.android.systemui.log.LogMessageImpl
+
+/**
+ * A simple implementation of [MessageBuffer] that forwards messages to [android.util.Log]
+ * immediately. This defeats the intention behind [LogBuffer] and should only be used when
+ * [LogBuffer]s are unavailable in a certain context.
+ */
+class LogcatOnlyMessageBuffer(
+    val targetLogLevel: LogLevel,
+) : MessageBuffer {
+    private val singleMessage = LogMessageImpl.Factory.create()
+    private var isObtained: Boolean = false
+
+    @Synchronized
+    override fun obtain(
+        tag: String,
+        level: LogLevel,
+        messagePrinter: MessagePrinter,
+        exception: Throwable?,
+    ): LogMessage {
+        if (isObtained) {
+            throw UnsupportedOperationException(
+                "Message has already been obtained. Call order is incorrect."
+            )
+        }
+
+        singleMessage.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception)
+        isObtained = true
+        return singleMessage
+    }
+
+    @Synchronized
+    override fun commit(message: LogMessage) {
+        if (singleMessage != message) {
+            throw IllegalArgumentException("Message argument is not the expected message.")
+        }
+        if (!isObtained) {
+            throw UnsupportedOperationException(
+                "Message has not been obtained. Call order is incorrect."
+            )
+        }
+
+        if (message.level >= targetLogLevel) {
+            val strMessage = message.messagePrinter(message)
+            when (message.level) {
+                LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception)
+                LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception)
+                LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception)
+                LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception)
+                LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception)
+                LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception)
+            }
+        }
+
+        isObtained = false
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
new file mode 100644
index 0000000..3f05fef
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.repository
+
+import android.os.UserHandle
+import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ColorInversionRepositoryImplTest : SysuiTestCase() {
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val scope = TestScope(testDispatcher)
+    private val settings: FakeSettings = FakeSettings()
+
+    private lateinit var underTest: ColorInversionRepository
+
+    @Before
+    fun setUp() {
+        underTest =
+            ColorInversionRepositoryImpl(
+                testDispatcher,
+                settings,
+            )
+    }
+
+    @Test
+    fun isEnabled_initiallyGetsSettingsValue() =
+        scope.runTest {
+            settings.putIntForUser(SETTING_NAME, 1, TEST_USER_1.identifier)
+
+            underTest =
+                ColorInversionRepositoryImpl(
+                    testDispatcher,
+                    settings,
+                )
+
+            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            runCurrent()
+
+            val actualValue: Boolean = underTest.isEnabled(TEST_USER_1).first()
+            assertThat(actualValue).isTrue()
+        }
+
+    @Test
+    fun isEnabled_settingUpdated_valueUpdated() =
+        scope.runTest {
+            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+
+            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            runCurrent()
+            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+
+            settings.putIntForUser(SETTING_NAME, ENABLED, TEST_USER_1.identifier)
+            runCurrent()
+            assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
+
+            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            runCurrent()
+            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+        }
+
+    @Test
+    fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() =
+        scope.runTest {
+            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            underTest.isEnabled(TEST_USER_2).launchIn(backgroundScope)
+            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_2.identifier)
+
+            runCurrent()
+            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+
+            settings.putIntForUser(SETTING_NAME, ENABLED, TEST_USER_1.identifier)
+            runCurrent()
+            assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
+            assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+        }
+
+    @Test
+    fun setEnabled() =
+        scope.runTest {
+            val success = underTest.setIsEnabled(true, TEST_USER_1)
+            runCurrent()
+            assertThat(success).isTrue()
+
+            val actualValue = settings.getIntForUser(SETTING_NAME, TEST_USER_1.identifier)
+            assertThat(actualValue).isEqualTo(ENABLED)
+        }
+
+    @Test
+    fun setDisabled() =
+        scope.runTest {
+            val success = underTest.setIsEnabled(false, TEST_USER_1)
+            runCurrent()
+            assertThat(success).isTrue()
+
+            val actualValue = settings.getIntForUser(SETTING_NAME, TEST_USER_1.identifier)
+            assertThat(actualValue).isEqualTo(DISABLED)
+        }
+
+    companion object {
+        private const val SETTING_NAME = ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
+        private const val DISABLED = 0
+        private const val ENABLED = 1
+        private val TEST_USER_1 = UserHandle.of(1)!!
+        private val TEST_USER_2 = UserHandle.of(2)!!
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index 64ddbc7..c961be9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -35,8 +35,10 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.function.Function
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runCurrent
@@ -58,6 +60,7 @@
 
     private val testUtils = SceneTestUtils(this)
     private val testScope = testUtils.testScope
+    private val clock = FakeSystemClock()
     private val userRepository = FakeUserRepository()
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
 
@@ -78,8 +81,10 @@
         underTest =
             AuthenticationRepositoryImpl(
                 applicationScope = testScope.backgroundScope,
-                getSecurityMode = getSecurityMode,
                 backgroundDispatcher = testUtils.testDispatcher,
+                flags = testUtils.sceneContainerFlags,
+                clock = clock,
+                getSecurityMode = getSecurityMode,
                 userRepository = userRepository,
                 lockPatternUtils = lockPatternUtils,
                 broadcastDispatcher = fakeBroadcastDispatcher,
@@ -141,22 +146,6 @@
         }
 
     @Test
-    fun reportAuthenticationAttempt_emitsAuthenticationChallengeResult() =
-        testScope.runTest {
-            val authenticationChallengeResults by
-                collectValues(underTest.authenticationChallengeResult)
-
-            runCurrent()
-            underTest.reportAuthenticationAttempt(true)
-            runCurrent()
-            underTest.reportAuthenticationAttempt(false)
-            runCurrent()
-            underTest.reportAuthenticationAttempt(true)
-
-            assertThat(authenticationChallengeResults).isEqualTo(listOf(true, false, true))
-        }
-
-    @Test
     fun isPinEnhancedPrivacyEnabled() =
         testScope.runTest {
             whenever(lockPatternUtils.isPinEnhancedPrivacyEnabled(USER_INFOS[0].id))
@@ -172,6 +161,45 @@
             assertThat(values.last()).isTrue()
         }
 
+    @Test
+    fun lockoutEndTimestamp() =
+        testScope.runTest {
+            val lockoutEndMs = clock.elapsedRealtime() + 30.seconds.inWholeMilliseconds
+            whenever(lockPatternUtils.getLockoutAttemptDeadline(USER_INFOS[0].id))
+                .thenReturn(lockoutEndMs)
+            whenever(lockPatternUtils.getLockoutAttemptDeadline(USER_INFOS[1].id)).thenReturn(0)
+
+            // Switch to a user who is not locked-out.
+            userRepository.setSelectedUserInfo(USER_INFOS[1])
+            assertThat(underTest.lockoutEndTimestamp).isNull()
+
+            // Switch back to the locked-out user, verify the timestamp is up-to-date.
+            userRepository.setSelectedUserInfo(USER_INFOS[0])
+            assertThat(underTest.lockoutEndTimestamp).isEqualTo(lockoutEndMs)
+
+            // After the lockout expires, null is returned.
+            clock.setElapsedRealtime(lockoutEndMs)
+            assertThat(underTest.lockoutEndTimestamp).isNull()
+        }
+
+    @Test
+    fun hasLockoutOccurred() =
+        testScope.runTest {
+            val hasLockoutOccurred by collectLastValue(underTest.hasLockoutOccurred)
+            assertThat(hasLockoutOccurred).isFalse()
+
+            underTest.reportLockoutStarted(1000)
+            assertThat(hasLockoutOccurred).isTrue()
+
+            clock.setElapsedRealtime(clock.elapsedRealtime() + 60.seconds.inWholeMilliseconds)
+
+            underTest.reportAuthenticationAttempt(isSuccessful = false)
+            assertThat(hasLockoutOccurred).isTrue()
+
+            underTest.reportAuthenticationAttempt(isSuccessful = true)
+            assertThat(hasLockoutOccurred).isFalse()
+        }
+
     private fun setSecurityModeAndDispatchBroadcast(
         securityMode: KeyguardSecurityModel.SecurityMode,
     ) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 08cd7ed..c113b37 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -21,14 +21,18 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.scene.SceneTestUtils
 import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -43,21 +47,23 @@
     private val testScope = utils.testScope
     private val underTest = utils.authenticationInteractor()
 
+    private val onAuthenticationResult by
+        testScope.collectLastValue(underTest.onAuthenticationResult)
+    private val failedAuthenticationAttempts by
+        testScope.collectLastValue(underTest.failedAuthenticationAttempts)
+
     @Test
     fun authenticationMethod() =
         testScope.runTest {
             val authMethod by collectLastValue(underTest.authenticationMethod)
             runCurrent()
-            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin)
-            assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Pin)
+            assertThat(authMethod).isEqualTo(Pin)
+            assertThat(underTest.getAuthenticationMethod()).isEqualTo(Pin)
 
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
+            utils.authenticationRepository.setAuthenticationMethod(Password)
 
-            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password)
-            assertThat(underTest.getAuthenticationMethod())
-                .isEqualTo(AuthenticationMethodModel.Password)
+            assertThat(authMethod).isEqualTo(Password)
+            assertThat(underTest.getAuthenticationMethod()).isEqualTo(Password)
         }
 
     @Test
@@ -66,51 +72,45 @@
             val authMethod by collectLastValue(underTest.authenticationMethod)
             runCurrent()
 
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            utils.authenticationRepository.setAuthenticationMethod(None)
 
-            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None)
-            assertThat(underTest.getAuthenticationMethod())
-                .isEqualTo(AuthenticationMethodModel.None)
+            assertThat(authMethod).isEqualTo(None)
+            assertThat(underTest.getAuthenticationMethod()).isEqualTo(None)
         }
 
     @Test
     fun authenticate_withCorrectPin_succeeds() =
         testScope.runTest {
-            val lockout by collectLastValue(underTest.lockout)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            utils.authenticationRepository.setAuthenticationMethod(Pin)
 
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(lockout).isNull()
-            assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
+            assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
         }
 
     @Test
     fun authenticate_withIncorrectPin_fails() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            utils.authenticationRepository.setAuthenticationMethod(Pin)
 
-            assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
-                .isEqualTo(AuthenticationResult.FAILED)
+            assertFailed(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
         }
 
     @Test(expected = IllegalArgumentException::class)
     fun authenticate_withEmptyPin_throwsException() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            utils.authenticationRepository.setAuthenticationMethod(Pin)
             underTest.authenticate(listOf())
         }
 
     @Test
     fun authenticate_withCorrectMaxLengthPin_succeeds() =
         testScope.runTest {
-            val pin = List(16) { 9 }
+            val correctMaxLengthPin = List(16) { 9 }
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
-                overrideCredential(pin)
+                setAuthenticationMethod(Pin)
+                overrideCredential(correctMaxLengthPin)
             }
 
-            assertThat(underTest.authenticate(pin)).isEqualTo(AuthenticationResult.SUCCEEDED)
+            assertSucceeded(underTest.authenticate(correctMaxLengthPin))
         }
 
     @Test
@@ -122,88 +122,64 @@
             // If the policy changes, there is work to do in SysUI.
             assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17)
 
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            assertThat(underTest.authenticate(List(17) { 9 }))
-                .isEqualTo(AuthenticationResult.FAILED)
+            utils.authenticationRepository.setAuthenticationMethod(Pin)
+
+            assertFailed(underTest.authenticate(List(17) { 9 }))
         }
 
     @Test
     fun authenticate_withCorrectPassword_succeeds() =
         testScope.runTest {
-            val lockout by collectLastValue(underTest.lockout)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
+            utils.authenticationRepository.setAuthenticationMethod(Password)
 
-            assertThat(underTest.authenticate("password".toList()))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(lockout).isNull()
-            assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
+            assertSucceeded(underTest.authenticate("password".toList()))
         }
 
     @Test
     fun authenticate_withIncorrectPassword_fails() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
+            utils.authenticationRepository.setAuthenticationMethod(Password)
 
-            assertThat(underTest.authenticate("alohomora".toList()))
-                .isEqualTo(AuthenticationResult.FAILED)
+            assertFailed(underTest.authenticate("alohomora".toList()))
         }
 
     @Test
     fun authenticate_withCorrectPattern_succeeds() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Pattern
-            )
+            utils.authenticationRepository.setAuthenticationMethod(Pattern)
 
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
+            assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
         }
 
     @Test
     fun authenticate_withIncorrectPattern_fails() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Pattern
-            )
-
-            assertThat(
-                    underTest.authenticate(
-                        listOf(
-                            AuthenticationPatternCoordinate(x = 2, y = 0),
-                            AuthenticationPatternCoordinate(x = 2, y = 1),
-                            AuthenticationPatternCoordinate(x = 2, y = 2),
-                            AuthenticationPatternCoordinate(x = 1, y = 2),
-                        )
-                    )
+            utils.authenticationRepository.setAuthenticationMethod(Pattern)
+            val wrongPattern =
+                listOf(
+                    AuthenticationPatternCoordinate(x = 2, y = 0),
+                    AuthenticationPatternCoordinate(x = 2, y = 1),
+                    AuthenticationPatternCoordinate(x = 2, y = 2),
+                    AuthenticationPatternCoordinate(x = 1, y = 2),
                 )
-                .isEqualTo(AuthenticationResult.FAILED)
+
+            assertFailed(underTest.authenticate(wrongPattern))
         }
 
     @Test
     fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNull() =
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
-            val lockout by collectLastValue(underTest.lockout)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(true)
             }
             assertThat(isAutoConfirmEnabled).isTrue()
+            val shorterPin =
+                FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { removeLast() }
 
-            assertThat(
-                    underTest.authenticate(
-                        FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply {
-                            removeLast()
-                        },
-                        tryAutoConfirm = true
-                    )
-                )
-                .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(lockout).isNull()
+            assertSkipped(underTest.authenticate(shorterPin, tryAutoConfirm = true))
+            assertThat(underTest.lockoutEndTimestamp).isNull()
             assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
         }
 
@@ -212,18 +188,17 @@
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(true)
             }
             assertThat(isAutoConfirmEnabled).isTrue()
 
-            assertThat(
-                    underTest.authenticate(
-                        FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 },
-                        tryAutoConfirm = true
-                    )
-                )
-                .isEqualTo(AuthenticationResult.FAILED)
+            val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+
+            assertFailed(
+                underTest.authenticate(wrongPin, tryAutoConfirm = true),
+                assertNoResultEvents = true,
+            )
         }
 
     @Test
@@ -231,18 +206,17 @@
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(true)
             }
             assertThat(isAutoConfirmEnabled).isTrue()
 
-            assertThat(
-                    underTest.authenticate(
-                        FakeAuthenticationRepository.DEFAULT_PIN + listOf(7),
-                        tryAutoConfirm = true
-                    )
-                )
-                .isEqualTo(AuthenticationResult.FAILED)
+            val longerPin = FakeAuthenticationRepository.DEFAULT_PIN + listOf(7)
+
+            assertFailed(
+                underTest.authenticate(longerPin, tryAutoConfirm = true),
+                assertNoResultEvents = true,
+            )
         }
 
     @Test
@@ -250,69 +224,54 @@
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(true)
             }
             assertThat(isAutoConfirmEnabled).isTrue()
 
-            assertThat(
-                    underTest.authenticate(
-                        FakeAuthenticationRepository.DEFAULT_PIN,
-                        tryAutoConfirm = true
-                    )
-                )
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
+            val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+            assertSucceeded(underTest.authenticate(correctPin, tryAutoConfirm = true))
         }
 
     @Test
     fun tryAutoConfirm_withAutoConfirmCorrectPinButDuringLockout_returnsNull() =
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
-            val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(true)
-                setLockoutDuration(42)
+                reportLockoutStarted(42)
             }
 
-            val authResult =
-                underTest.authenticate(
-                    FakeAuthenticationRepository.DEFAULT_PIN,
-                    tryAutoConfirm = true
-                )
+            val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
 
-            assertThat(authResult).isEqualTo(AuthenticationResult.SKIPPED)
+            assertSkipped(underTest.authenticate(correctPin, tryAutoConfirm = true))
             assertThat(isAutoConfirmEnabled).isFalse()
-            assertThat(isUnlocked).isFalse()
             assertThat(hintedPinLength).isNull()
+            assertThat(underTest.lockoutEndTimestamp).isNotNull()
         }
 
     @Test
     fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNull() =
         testScope.runTest {
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(false)
             }
-            assertThat(
-                    underTest.authenticate(
-                        FakeAuthenticationRepository.DEFAULT_PIN,
-                        tryAutoConfirm = true
-                    )
-                )
-                .isEqualTo(AuthenticationResult.SKIPPED)
+
+            val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+            assertSkipped(underTest.authenticate(correctPin, tryAutoConfirm = true))
         }
 
     @Test
     fun tryAutoConfirm_withoutCorrectPassword_returnsNull() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
+            utils.authenticationRepository.setAuthenticationMethod(Password)
 
-            assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true))
-                .isEqualTo(AuthenticationResult.SKIPPED)
+            assertSkipped(underTest.authenticate("password".toList(), tryAutoConfirm = true))
         }
 
     @Test
@@ -337,7 +296,6 @@
     fun isAutoConfirmEnabled_featureEnabledButDisabledByLockout() =
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
-            val lockout by collectLastValue(underTest.lockout)
             utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
 
             // The feature is enabled.
@@ -345,92 +303,104 @@
 
             // Make many wrong attempts to trigger lockout.
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
+                assertFailed(underTest.authenticate(listOf(5, 6, 7))) // Wrong PIN
             }
-            assertThat(lockout).isNotNull()
+            assertThat(underTest.lockoutEndTimestamp).isNotNull()
             assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1)
 
             // Lockout disabled auto-confirm.
             assertThat(isAutoConfirmEnabled).isFalse()
 
             // Move the clock forward one more second, to completely finish the lockout period:
-            advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_MS + 1000L)
-            assertThat(lockout).isNull()
+            advanceTimeBy(
+                FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds.plus(1.seconds)
+            )
+            assertThat(underTest.lockoutEndTimestamp).isNull()
 
             // Auto-confirm is still disabled, because lockout occurred at least once in this
             // session.
             assertThat(isAutoConfirmEnabled).isFalse()
 
             // Correct PIN and unlocks successfully, resetting the 'session'.
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
+            assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
 
             // Auto-confirm is re-enabled.
             assertThat(isAutoConfirmEnabled).isTrue()
-
-            assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1)
         }
 
     @Test
-    fun lockout() =
+    fun failedAuthenticationAttempts() =
         testScope.runTest {
-            val lockout by collectLastValue(underTest.lockout)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
-            assertThat(lockout).isNull()
+            val failedAuthenticationAttempts by
+                collectLastValue(underTest.failedAuthenticationAttempts)
+
+            utils.authenticationRepository.setAuthenticationMethod(Pin)
+            val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+            assertSucceeded(underTest.authenticate(correctPin))
+            assertThat(failedAuthenticationAttempts).isEqualTo(0)
+
+            // Make many wrong attempts, leading to lockout:
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { index ->
+                underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
+                assertThat(failedAuthenticationAttempts).isEqualTo(index + 1)
+            }
+
+            // Correct PIN, but locked out, so doesn't attempt it:
+            assertSkipped(underTest.authenticate(correctPin), assertNoResultEvents = false)
+            assertThat(failedAuthenticationAttempts)
+                .isEqualTo(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT)
+
+            // Move the clock forward to finish the lockout period:
+            advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
+            assertThat(failedAuthenticationAttempts)
+                .isEqualTo(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT)
+
+            // Correct PIN and no longer locked out so unlocks successfully:
+            assertSucceeded(underTest.authenticate(correctPin))
+            assertThat(failedAuthenticationAttempts).isEqualTo(0)
+        }
+
+    @Test
+    fun lockoutEndTimestamp() =
+        testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(Pin)
+            val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+            underTest.authenticate(correctPin)
+            assertThat(underTest.lockoutEndTimestamp).isNull()
 
             // Make many wrong attempts, but just shy of what's needed to get locked out:
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
                 underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
-                assertThat(lockout).isNull()
+                assertThat(underTest.lockoutEndTimestamp).isNull()
             }
 
             // Make one more wrong attempt, leading to lockout:
             underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
-            assertThat(lockout)
-                .isEqualTo(
-                    AuthenticationLockoutModel(
-                        failedAttemptCount =
-                            FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
-                        remainingSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS,
-                    )
-                )
+
+            val expectedLockoutEndTimestamp =
+                testScope.currentTime + FakeAuthenticationRepository.LOCKOUT_DURATION_MS
+            assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp)
             assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1)
 
             // Correct PIN, but locked out, so doesn't attempt it:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(lockout)
-                .isEqualTo(
-                    AuthenticationLockoutModel(
-                        failedAttemptCount =
-                            FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
-                        remainingSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS,
-                    )
-                )
+            assertSkipped(underTest.authenticate(correctPin), assertNoResultEvents = false)
+            assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp)
 
             // Move the clock forward to ALMOST skip the lockout, leaving one second to go:
-            val lockoutTimeoutSec = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
-            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - 1) { time ->
-                advanceTimeBy(1000)
-                assertThat(lockout)
-                    .isEqualTo(
-                        AuthenticationLockoutModel(
-                            failedAttemptCount =
-                                FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
-                            remainingSeconds = lockoutTimeoutSec - (time + 1),
-                        )
-                    )
+            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - 1) {
+                advanceTimeBy(1.seconds)
+                assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp)
             }
 
             // Move the clock forward one more second, to completely finish the lockout period:
-            advanceTimeBy(1000)
-            assertThat(lockout).isNull()
+            advanceTimeBy(1.seconds)
+            assertThat(underTest.lockoutEndTimestamp).isNull()
 
             // Correct PIN and no longer locked out so unlocks successfully:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(lockout).isNull()
+            assertSucceeded(underTest.authenticate(correctPin))
+            assertThat(underTest.lockoutEndTimestamp).isNull()
         }
 
     @Test
@@ -438,7 +408,7 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(false)
             }
 
@@ -450,7 +420,7 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 overrideCredential(
                     buildList {
                         repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) }
@@ -467,7 +437,7 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 setAutoConfirmFeatureEnabled(true)
                 overrideCredential(
                     buildList {
@@ -484,7 +454,7 @@
         testScope.runTest {
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
             utils.authenticationRepository.apply {
-                setAuthenticationMethod(AuthenticationMethodModel.Pin)
+                setAuthenticationMethod(Pin)
                 overrideCredential(
                     buildList {
                         repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) }
@@ -499,18 +469,45 @@
     @Test
     fun authenticate_withTooShortPassword() =
         testScope.runTest {
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            assertThat(
-                    underTest.authenticate(
-                        buildList {
-                            repeat(utils.authenticationRepository.minPasswordLength - 1) { time ->
-                                add("$time")
-                            }
-                        }
-                    )
-                )
-                .isEqualTo(AuthenticationResult.SKIPPED)
+            utils.authenticationRepository.setAuthenticationMethod(Password)
+
+            val tooShortPassword = buildList {
+                repeat(utils.authenticationRepository.minPasswordLength - 1) { time ->
+                    add("$time")
+                }
+            }
+            assertSkipped(underTest.authenticate(tooShortPassword))
         }
+
+    private fun assertSucceeded(authenticationResult: AuthenticationResult) {
+        assertThat(authenticationResult).isEqualTo(AuthenticationResult.SUCCEEDED)
+        assertThat(onAuthenticationResult).isTrue()
+        assertThat(underTest.lockoutEndTimestamp).isNull()
+        assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
+        assertThat(failedAuthenticationAttempts).isEqualTo(0)
+    }
+
+    private fun assertFailed(
+        authenticationResult: AuthenticationResult,
+        assertNoResultEvents: Boolean = false,
+    ) {
+        assertThat(authenticationResult).isEqualTo(AuthenticationResult.FAILED)
+        if (assertNoResultEvents) {
+            assertThat(onAuthenticationResult).isNull()
+        } else {
+            assertThat(onAuthenticationResult).isFalse()
+        }
+    }
+
+    private fun assertSkipped(
+        authenticationResult: AuthenticationResult,
+        assertNoResultEvents: Boolean = true,
+    ) {
+        assertThat(authenticationResult).isEqualTo(AuthenticationResult.SKIPPED)
+        if (assertNoResultEvents) {
+            assertThat(onAuthenticationResult).isNull()
+        } else {
+            assertThat(onAuthenticationResult).isNotNull()
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index da97a12..1c1335f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -923,14 +923,14 @@
         doReturn(500).when(mResources)
                 .getDimensionPixelSize(eq(com.android.systemui.res.R.dimen
                         .physical_fingerprint_sensor_center_screen_location_y));
-        mAuthController.onConfigurationChanged(null /* newConfig */);
+        mAuthController.onConfigChanged(null /* newConfig */);
 
         final Point firstFpLocation = mAuthController.getFingerprintSensorLocation();
 
         doReturn(1000).when(mResources)
                 .getDimensionPixelSize(eq(com.android.systemui.res.R.dimen
                         .physical_fingerprint_sensor_center_screen_location_y));
-        mAuthController.onConfigurationChanged(null /* newConfig */);
+        mAuthController.onConfigChanged(null /* newConfig */);
 
         assertNotSame(firstFpLocation, mAuthController.getFingerprintSensorLocation());
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index a726b7c..b0beab9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -47,6 +47,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
@@ -54,7 +55,6 @@
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
@@ -101,7 +101,6 @@
     @Mock
     private lateinit var unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController
     @Mock private lateinit var udfpsDisplayMode: UdfpsDisplayModeProvider
-    @Mock private lateinit var secureSettings: SecureSettings
     @Mock private lateinit var controllerCallback: IUdfpsOverlayControllerCallback
     @Mock private lateinit var udfpsController: UdfpsController
     @Mock private lateinit var udfpsView: UdfpsView
@@ -117,6 +116,7 @@
     @Mock
     private lateinit var udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    @Mock private lateinit var shadeInteractor: ShadeInteractor
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -174,6 +174,7 @@
                 mSelectedUserInteractor,
                 { deviceEntryUdfpsTouchOverlayViewModel },
                 { defaultUdfpsTouchOverlayViewModel },
+                shadeInteractor
             )
         block()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index dddcf18..e5da1f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -90,11 +90,11 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -124,8 +124,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.inject.Provider;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 @RunWithLooper(setAsMainLooper = true)
@@ -208,6 +206,8 @@
     @Mock
     private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     @Mock
+    private ShadeInteractor mShadeInteractor;
+    @Mock
     private SinglePointerTouchProcessor mSinglePointerTouchProcessor;
     @Mock
     private SessionTracker mSessionTracker;
@@ -216,8 +216,6 @@
     @Mock
     private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     @Mock
-    private Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels;
-    @Mock
     private SelectedUserInteractor mSelectedUserInteractor;
 
     // Capture listeners so that they can be used to send events
@@ -333,13 +331,13 @@
                 mActivityLaunchAnimator,
                 mBiometricExecutor,
                 mPrimaryBouncerInteractor,
+                mShadeInteractor,
                 mSinglePointerTouchProcessor,
                 mSessionTracker,
                 mAlternateBouncerInteractor,
                 mInputManager,
                 mock(KeyguardFaceAuthInteractor.class),
                 mUdfpsKeyguardAccessibilityDelegate,
-                mUdfpsKeyguardViewModels,
                 mSelectedUserInteractor,
                 mFpsUnlockTracker,
                 mKeyguardTransitionInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
index ac16c13..13b53a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
@@ -36,6 +36,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
@@ -70,6 +71,7 @@
     protected @Mock UdfpsController mUdfpsController;
     protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
     protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    protected @Mock ShadeInteractor mShadeInteractor;
     protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
     protected @Mock UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     protected @Mock SelectedUserInteractor mSelectedUserInteractor;
@@ -149,7 +151,8 @@
                 mAlternateBouncerInteractor,
                 mUdfpsKeyguardAccessibilityDelegate,
                 mSelectedUserInteractor,
-                mKeyguardTransitionInteractor);
+                mKeyguardTransitionInteractor,
+                mShadeInteractor);
         return controller;
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 0ab596c..1f8854f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -616,4 +616,28 @@
                 .onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN))
             job.cancel()
         }
+
+    @Test
+    fun bouncerToAod_dozeAmountChanged() =
+        testScope.runTest {
+            // GIVEN view is attached
+            mController.onViewAttached()
+            Mockito.reset(mView)
+
+            val job = mController.listenForPrimaryBouncerToAodTransitions(this)
+            // WHEN alternate bouncer to aod transition in progress
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.AOD,
+                    value = .3f,
+                    transitionState = TransitionState.RUNNING
+                )
+            )
+            runCurrent()
+
+            // THEN doze amount is updated to
+            verify(mView).onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATE_APPEAR_ON_SCREEN_OFF))
+            job.cancel()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
index 0870122..adf4fc6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -32,18 +32,15 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AlternateBouncerInteractorTest : SysuiTestCase() {
@@ -167,7 +164,6 @@
     }
 
     @Test
-    @Ignore("b/287599719")
     fun canShowAlternateBouncerForFingerprint_rearFps() {
         mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         initializeUnderTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 9b1df7c..99c1874 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -21,15 +21,15 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
 import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.google.common.truth.Truth.assertThat
-import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
@@ -79,7 +79,7 @@
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             runCurrent()
             underTest.clearMessage()
-            assertThat(message).isEmpty()
+            assertThat(message).isNull()
 
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -149,7 +149,7 @@
             // Incomplete input.
             assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEmpty()
+            assertThat(message).isNull()
 
             // Correct input.
             assertThat(
@@ -159,7 +159,7 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEmpty()
+            assertThat(message).isNull()
         }
 
     @Test
@@ -246,57 +246,40 @@
         }
 
     @Test
-    fun lockout() =
+    fun lockoutStarted() =
         testScope.runTest {
-            val lockout by collectLastValue(underTest.lockout)
+            val lockoutStartedEvents by collectValues(underTest.onLockoutStarted)
             val message by collectLastValue(underTest.message)
+
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            assertThat(lockout).isNull()
+            assertThat(lockoutStartedEvents).isEmpty()
+
+            // Try the wrong PIN repeatedly, until lockout is triggered:
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
                 // Wrong PIN.
                 assertThat(underTest.authenticate(listOf(6, 7, 8, 9)))
                     .isEqualTo(AuthenticationResult.FAILED)
                 if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
-                    assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+                    assertThat(lockoutStartedEvents).isEmpty()
+                    assertThat(message).isNotEmpty()
                 }
             }
-            assertThat(lockout)
-                .isEqualTo(
-                    AuthenticationLockoutModel(
-                        failedAttemptCount =
-                            FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
-                        remainingSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS,
-                    )
-                )
-            assertTryAgainMessage(
-                message,
-                FakeAuthenticationRepository.LOCKOUT_DURATION_MS.milliseconds.inWholeSeconds.toInt()
-            )
+            assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull()
+            assertThat(lockoutStartedEvents.size).isEqualTo(1)
+            assertThat(message).isNull()
 
-            // Correct PIN, but locked out, so doesn't change away from the bouncer scene:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SKIPPED)
-            assertTryAgainMessage(
-                message,
-                FakeAuthenticationRepository.LOCKOUT_DURATION_MS.milliseconds.inWholeSeconds.toInt()
-            )
+            // Advance the time to finish the lockout:
+            advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
+            assertThat(authenticationInteractor.lockoutEndTimestamp).isNull()
+            assertThat(message).isNull()
+            assertThat(lockoutStartedEvents.size).isEqualTo(1)
 
-            lockout?.remainingSeconds?.let { seconds ->
-                repeat(seconds) { time ->
-                    advanceTimeBy(1000)
-                    val remainingTimeSec = seconds - time - 1
-                    if (remainingTimeSec > 0) {
-                        assertTryAgainMessage(message, remainingTimeSec)
-                    }
-                }
+            // Trigger lockout again:
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
+                // Wrong PIN.
+                underTest.authenticate(listOf(6, 7, 8, 9))
             }
-            assertThat(message).isEqualTo("")
-            assertThat(lockout).isNull()
-
-            // Correct PIN and no longer locked out so changes to the Gone scene:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
-            assertThat(lockout).isNull()
+            assertThat(lockoutStartedEvents.size).isEqualTo(2)
         }
 
     @Test
@@ -326,13 +309,6 @@
             verify(keyguardFaceAuthInteractor).onPrimaryBouncerUserInput()
         }
 
-    private fun assertTryAgainMessage(
-        message: String?,
-        time: Int,
-    ) {
-        assertThat(message).isEqualTo("Try again in $time seconds.")
-    }
-
     companion object {
         private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
         private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 16a9359..4be9b0a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -33,6 +33,7 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -135,19 +136,47 @@
     fun message() =
         testScope.runTest {
             val message by collectLastValue(underTest.message)
-            val lockout by collectLastValue(bouncerInteractor.lockout)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(message?.isUpdateAnimated).isTrue()
 
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                // Wrong PIN.
-                bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+                bouncerInteractor.authenticate(WRONG_PIN)
             }
             assertThat(message?.isUpdateAnimated).isFalse()
 
-            lockout?.remainingSeconds?.let { remainingSeconds ->
-                advanceTimeBy(remainingSeconds.seconds.inWholeMilliseconds)
+            val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+            advanceTimeBy(lockoutEndMs - testScope.currentTime)
+            assertThat(message?.isUpdateAnimated).isTrue()
+        }
+
+    @Test
+    fun lockoutMessage() =
+        testScope.runTest {
+            val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
+            val message by collectLastValue(underTest.message)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            assertThat(utils.authenticationRepository.lockoutEndTimestamp).isNull()
+            assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
+
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
+                bouncerInteractor.authenticate(WRONG_PIN)
+                if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
+                    assertThat(message?.text).isEqualTo(bouncerInteractor.message.value)
+                    assertThat(message?.isUpdateAnimated).isTrue()
+                }
             }
+            val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
+            assertTryAgainMessage(message?.text, lockoutSeconds)
+            assertThat(message?.isUpdateAnimated).isFalse()
+
+            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
+                advanceTimeBy(1.seconds)
+                val remainingSeconds = lockoutSeconds - time - 1
+                if (remainingSeconds > 0) {
+                    assertTryAgainMessage(message?.text, remainingSeconds)
+                }
+            }
+            assertThat(message?.text).isEmpty()
             assertThat(message?.isUpdateAnimated).isTrue()
         }
 
@@ -160,32 +189,30 @@
                         authViewModel?.isInputEnabled ?: emptyFlow()
                     }
                 )
-            val lockout by collectLastValue(bouncerInteractor.lockout)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
             assertThat(isInputEnabled).isTrue()
 
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                // Wrong PIN.
-                bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+                bouncerInteractor.authenticate(WRONG_PIN)
             }
             assertThat(isInputEnabled).isFalse()
 
-            lockout?.remainingSeconds?.let { remainingSeconds ->
-                advanceTimeBy(remainingSeconds.seconds.inWholeMilliseconds)
-            }
+            val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+            advanceTimeBy(lockoutEndMs - testScope.currentTime)
             assertThat(isInputEnabled).isTrue()
         }
 
     @Test
     fun dialogMessage() =
         testScope.runTest {
+            val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
             val dialogMessage by collectLastValue(underTest.dialogMessage)
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
 
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                // Wrong PIN.
                 assertThat(dialogMessage).isNull()
-                bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+                bouncerInteractor.authenticate(WRONG_PIN)
             }
             assertThat(dialogMessage).isNotEmpty()
 
@@ -241,4 +268,15 @@
             AuthenticationMethodModel.Sim,
         )
     }
+
+    private fun assertTryAgainMessage(
+        message: String?,
+        time: Int,
+    ) {
+        assertThat(message).isEqualTo("Try again in $time seconds.")
+    }
+
+    companion object {
+        private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 6d6baa5..64e6e57 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -19,7 +19,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
@@ -28,6 +27,7 @@
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -45,11 +45,7 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val authenticationRepository = utils.authenticationRepository
-    private val authenticationInteractor =
-        utils.authenticationInteractor(
-            repository = authenticationRepository,
-        )
+    private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
     private val bouncerInteractor =
         utils.bouncerInteractor(
@@ -61,12 +57,13 @@
             authenticationInteractor = authenticationInteractor,
             actionButtonInteractor = utils.bouncerActionButtonInteractor(),
         )
+    private val isInputEnabled = MutableStateFlow(true)
 
     private val underTest =
         PasswordBouncerViewModel(
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
-            isInputEnabled = MutableStateFlow(true).asStateFlow(),
+            isInputEnabled.asStateFlow(),
         )
 
     @Before
@@ -123,8 +120,7 @@
     @Test
     fun onAuthenticateKeyPressed_whenCorrect() =
         testScope.runTest {
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
@@ -169,8 +165,7 @@
     @Test
     fun onAuthenticateKeyPressed_correctAfterWrong() =
         testScope.runTest {
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
@@ -333,19 +328,15 @@
     ) {
         if (isLockedOut) {
             repeat(failedAttemptCount) {
-                authenticationRepository.reportAuthenticationAttempt(false)
+                utils.authenticationRepository.reportAuthenticationAttempt(false)
             }
-            val remainingTimeSeconds = 30
-            authenticationRepository.setLockoutDuration(remainingTimeSeconds * 1000)
-            authenticationRepository.lockout.value =
-                AuthenticationLockoutModel(
-                    failedAttemptCount = failedAttemptCount,
-                    remainingSeconds = remainingTimeSeconds,
-                )
+            utils.authenticationRepository.reportLockoutStarted(
+                30.seconds.inWholeMilliseconds.toInt()
+            )
         } else {
-            authenticationRepository.reportAuthenticationAttempt(true)
-            authenticationRepository.lockout.value = null
+            utils.authenticationRepository.reportAuthenticationAttempt(true)
         }
+        isInputEnabled.value = !isLockedOut
 
         runCurrent()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 8971423..ed76099 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -111,8 +111,7 @@
     @Test
     fun onDragEnd_whenCorrect() =
         testScope.runTest {
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
@@ -334,8 +333,7 @@
     @Test
     fun onDragEnd_correctAfterWrong() =
         testScope.runTest {
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index c30e405..db98d76 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -201,8 +201,7 @@
     @Test
     fun onAuthenticateButtonClicked_whenCorrect() =
         testScope.runTest {
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             lockDeviceAndOpenPinBouncer()
 
             FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
@@ -236,8 +235,7 @@
     @Test
     fun onAuthenticateButtonClicked_correctAfterWrong() =
         testScope.runTest {
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
@@ -265,8 +263,7 @@
     fun onAutoConfirm_whenCorrect() =
         testScope.runTest {
             utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
-            val authResult by
-                collectLastValue(authenticationInteractor.authenticationChallengeResult)
+            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             lockDeviceAndOpenPinBouncer()
 
             FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 182712a..ddaa488 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -208,11 +208,11 @@
             val repository = initCommunalWidgetRepository()
             runCurrent()
 
-            val ids = listOf(104, 103, 101)
-            repository.updateWidgetOrder(ids)
+            val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3)
+            repository.updateWidgetOrder(widgetIdToPriorityMap)
             runCurrent()
 
-            verify(communalWidgetDao).updateWidgetOrder(ids)
+            verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index d3049d9..565049b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -99,7 +99,7 @@
         }
 
     @Test
-    fun reportSuccessfulAuthentication_shouldUpdateIsUnlocked() =
+    fun reportSuccessfulAuthentication_updatesIsUnlocked() =
         testScope.runTest {
             val isUnlocked by collectLastValue(underTest.isUnlocked)
             assertThat(isUnlocked).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index 910097e..ea19cb7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
@@ -346,12 +347,14 @@
         }
 
     @Test
-    fun successfulAuthenticationChallengeAttempt_updatedIsUnlockedState() =
+    fun successfulAuthenticationChallengeAttempt_updatesIsUnlockedState() =
         testScope.runTest {
             val isUnlocked by collectLastValue(underTest.isUnlocked)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            utils.deviceEntryRepository.setLockscreenEnabled(true)
             assertThat(isUnlocked).isFalse()
 
-            utils.authenticationRepository.reportAuthenticationAttempt(true)
+            authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
 
             assertThat(isUnlocked).isTrue()
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
new file mode 100644
index 0000000..9daf186
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AlternateBouncerToAodTransitionViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+    private val biometricSettingsRepository = kosmos.biometricSettingsRepository
+    private val underTest = kosmos.alternateBouncerToAodTransitionViewModel
+
+    @Test
+    fun deviceEntryParentViewAppear() =
+        testScope.runTest {
+            fingerprintPropertyRepository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+                sensorLocations = emptyMap(),
+            )
+            biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            val values by collectValues(underTest.deviceEntryParentViewAlpha)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0f),
+                    step(0.1f),
+                    step(0.2f),
+                    step(0.3f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            values.forEach { assertThat(it).isEqualTo(1f) }
+        }
+
+    @Test
+    fun deviceEntryBackgroundViewDisappear() =
+        testScope.runTest {
+            val values by collectValues(underTest.deviceEntryBackgroundViewAlpha)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0f),
+                    step(0.1f),
+                    step(0.2f),
+                    step(0.3f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            assertThat(values.size).isEqualTo(6)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+        }
+
+    private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.ALTERNATE_BOUNCER,
+            to = AOD,
+            value = value,
+            transitionState = state,
+            ownerName = "AlternateBouncerToAodTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
new file mode 100644
index 0000000..3f7e0df
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AlternateBouncerToGoneTransitionViewModelTest : SysuiTestCase() {
+    val kosmos =
+        testKosmos().apply {
+            fakeFeatureFlagsClassic.apply {
+                set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false)
+                set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+            }
+        }
+    private val testScope = kosmos.testScope
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val underTest = kosmos.alternateBouncerToGoneTransitionViewModel
+
+    @Test
+    fun deviceEntryParentViewDisappear() =
+        testScope.runTest {
+            val values by collectValues(underTest.deviceEntryParentViewAlpha)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0f),
+                    step(0.1f),
+                    step(0.2f),
+                    step(0.3f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            values.forEach { assertThat(it).isEqualTo(0f) }
+        }
+
+    private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.ALTERNATE_BOUNCER,
+            to = GONE,
+            value = value,
+            transitionState = state,
+            ownerName = "AlternateBouncerToGoneTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
index cf076c5..a84b9fa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
@@ -24,16 +24,19 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.external.TileServiceKey
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
 import com.android.systemui.qs.tiles.impl.custom.commons.copy
+import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -44,194 +47,260 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class CustomTileRepositoryTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-
-    private val persister = FakeCustomTileStatePersister()
-
+    private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) }
     private val underTest: CustomTileRepository =
-        CustomTileRepositoryImpl(
-            TileSpec.create(TEST_COMPONENT),
-            persister,
-            testScope.testScheduler,
-        )
+        with(kosmos) {
+            CustomTileRepositoryImpl(
+                tileSpec,
+                customTileStatePersister,
+                packageManagerAdapterFacade.packageManagerAdapter,
+                testScope.testScheduler,
+            )
+        }
 
     @Test
     fun persistableTileIsRestoredForUser() =
-        testScope.runTest {
-            persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
-            persister.persistState(TEST_TILE_KEY_2, TEST_TILE_2)
+        with(kosmos) {
+            testScope.runTest {
+                customTileStatePersister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
+                customTileStatePersister.persistState(TEST_TILE_KEY_2, TEST_TILE_2)
 
-            underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
-            runCurrent()
+                underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun notPersistableTileIsNotRestored() =
-        testScope.runTest {
-            persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
-            val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+        with(kosmos) {
+            testScope.runTest {
+                customTileStatePersister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
 
-            underTest.restoreForTheUserIfNeeded(TEST_USER_1, false)
-            runCurrent()
+                underTest.restoreForTheUserIfNeeded(TEST_USER_1, false)
+                runCurrent()
 
-            assertThat(tiles()).isEmpty()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun emptyPersistedStateIsHandled() =
-        testScope.runTest {
-            val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+        with(kosmos) {
+            testScope.runTest {
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
 
-            underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
-            runCurrent()
+                underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
+                runCurrent()
 
-            assertThat(tiles()).isEmpty()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun updatingWithPersistableTilePersists() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1)
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1))
+                    .isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun updatingWithNotPersistableTileDoesntPersist() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull()
+            }
         }
 
     @Test
     fun updateWithTileEmits() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun updatingPeristableWithDefaultsPersists() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1)
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1))
+                    .isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun updatingNotPersistableWithDefaultsDoesntPersist() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull()
+            }
         }
 
     @Test
     fun updatingPeristableWithErrorDefaultsDoesntPersist() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true)
+                runCurrent()
 
-            assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+                assertThat(customTileStatePersister.readState(TEST_TILE_KEY_1)).isNull()
+            }
         }
 
     @Test
     fun updateWithDefaultsEmits() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+            }
         }
 
     @Test
     fun getTileForAnotherUserReturnsNull() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_2)).isNull()
+                assertThat(underTest.getTile(TEST_USER_2)).isNull()
+            }
         }
 
     @Test
     fun getTilesForAnotherUserEmpty() =
-        testScope.runTest {
-            val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+        with(kosmos) {
+            testScope.runTest {
+                val tiles = collectValues(underTest.getTiles(TEST_USER_2))
 
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            assertThat(tiles()).isEmpty()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun updatingWithTileForTheSameUserAddsData() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true)
-            runCurrent()
+                underTest.updateWithTile(
+                    TEST_USER_1,
+                    Tile().apply { subtitle = "test_subtitle" },
+                    true
+                )
+                runCurrent()
 
-            val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+                val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+            }
         }
 
     @Test
     fun updatingWithTileForAnotherUserOverridesTile() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+                runCurrent()
 
-            val tiles = collectValues(underTest.getTiles(TEST_USER_2))
-            underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true)
-            runCurrent()
+                val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+                underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+                assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+            }
         }
 
     @Test
     fun updatingWithDefaultsForTheSameUserAddsData() =
-        testScope.runTest {
-            underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithTile(
+                    TEST_USER_1,
+                    Tile().apply { subtitle = "test_subtitle" },
+                    true
+                )
+                runCurrent()
 
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
-            assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
-            assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+                val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+            }
         }
 
     @Test
     fun updatingWithDefaultsForAnotherUserOverridesTile() =
-        testScope.runTest {
-            underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+                runCurrent()
 
-            val tiles = collectValues(underTest.getTiles(TEST_USER_2))
-            underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true)
-            runCurrent()
+                val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+                underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true)
+                runCurrent()
 
-            assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+                assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+            }
+        }
+
+    @Test
+    fun isActiveFollowsPackageManagerAdapter() =
+        with(kosmos) {
+            testScope.runTest {
+                packageManagerAdapterFacade.setIsActive(false)
+                assertThat(underTest.isTileActive()).isFalse()
+
+                packageManagerAdapterFacade.setIsActive(true)
+                assertThat(underTest.isTileActive()).isTrue()
+            }
+        }
+
+    @Test
+    fun isToggleableFollowsPackageManagerAdapter() =
+        with(kosmos) {
+            testScope.runTest {
+                packageManagerAdapterFacade.setIsToggleable(false)
+                assertThat(underTest.isTileToggleable()).isFalse()
+
+                packageManagerAdapterFacade.setIsToggleable(true)
+                assertThat(underTest.isTileToggleable()).isTrue()
+            }
         }
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
index eebb145..90779cb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
@@ -25,157 +25,159 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.external.TileServiceKey
-import com.android.systemui.qs.external.TileServiceManager
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.customTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
-import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
-import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 class CustomTileInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var tileServiceManager: TileServiceManager
+    private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) }
 
-    private val testScope = TestScope()
-
-    private val defaultsRepository = FakeCustomTileDefaultsRepository()
-    private val customTileStatePersister = FakeCustomTileStatePersister()
-    private val customTileRepository =
-        FakeCustomTileRepository(
-            TEST_TILE_SPEC,
-            customTileStatePersister,
-            testScope.testScheduler,
-        )
-
-    private lateinit var underTest: CustomTileInteractor
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
+    private val underTest: CustomTileInteractor =
+        with(kosmos) {
             CustomTileInteractor(
-                TEST_USER,
-                defaultsRepository,
+                customTileDefaultsRepository,
                 customTileRepository,
-                tileServiceManager,
                 testScope.backgroundScope,
                 testScope.testScheduler,
             )
-    }
+        }
 
     @Test
     fun activeTileIsAvailableAfterRestored() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(true)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(true)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
 
-            underTest.init()
+                underTest.initForUser(TEST_USER)
 
-            assertThat(underTest.tile).isEqualTo(TEST_TILE)
-            assertThat(underTest.tiles.first()).isEqualTo(TEST_TILE)
+                assertThat(underTest.getTile(TEST_USER)).isEqualTo(TEST_TILE)
+                assertThat(underTest.getTiles(TEST_USER).first()).isEqualTo(TEST_TILE)
+            }
         }
 
     @Test
     fun notActiveTileIsAvailableAfterUpdated() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(false)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
-            val tiles = collectValues(underTest.tiles)
-            val initJob = launch { underTest.init() }
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
+                val initJob = launch { underTest.initForUser(TEST_USER) }
 
-            underTest.updateTile(TEST_TILE)
-            runCurrent()
-            initJob.join()
+                underTest.updateTile(TEST_TILE)
+                runCurrent()
+                initJob.join()
 
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE)
+            }
         }
 
     @Test
     fun notActiveTileIsAvailableAfterDefaultsUpdated() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(false)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
-            val tiles = collectValues(underTest.tiles)
-            val initJob = launch { underTest.init() }
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
+                val initJob = launch { underTest.initForUser(TEST_USER) }
 
-            defaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS)
-            defaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT)
-            runCurrent()
-            initJob.join()
+                customTileDefaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS)
+                customTileDefaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT)
+                runCurrent()
+                initJob.join()
 
-            assertThat(tiles()).hasSize(1)
-            assertThat(tiles().last()).isEqualTo(TEST_TILE)
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE)
+            }
         }
 
     @Test(expected = IllegalStateException::class)
-    fun getTileBeforeInitThrows() = testScope.runTest { underTest.tile }
+    fun getTileBeforeInitThrows() =
+        with(kosmos) { testScope.runTest { underTest.getTile(TEST_USER) } }
 
     @Test
     fun initSuspendsForActiveTileNotRestoredAndNotUpdated() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(true)
-            val tiles = collectValues(underTest.tiles)
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(true)
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
 
-            val initJob = backgroundScope.launch { underTest.init() }
-            advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
+                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) }
+                advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
 
-            // Is still suspended
-            assertThat(initJob.isActive).isTrue()
-            assertThat(tiles()).isEmpty()
+                // Is still suspended
+                assertThat(initJob.isActive).isTrue()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
     @Test
     fun initSuspendedForNotActiveTileWithoutUpdates() =
-        testScope.runTest {
-            whenever(tileServiceManager.isActiveTile).thenReturn(false)
-            customTileStatePersister.persistState(
-                TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                TEST_TILE,
-            )
-            val tiles = collectValues(underTest.tiles)
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+                    TEST_TILE,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER))
 
-            val initJob = backgroundScope.launch { underTest.init() }
-            advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
+                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) }
+                advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
 
-            // Is still suspended
-            assertThat(initJob.isActive).isTrue()
-            assertThat(tiles()).isEmpty()
+                // Is still suspended
+                assertThat(initJob.isActive).isTrue()
+                assertThat(tiles()).isEmpty()
+            }
         }
 
+    @Test
+    fun toggleableFollowsTheRepository() {
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileToggleable(false)
+                assertThat(underTest.isTileToggleable()).isFalse()
+
+                customTileRepository.setTileToggleable(true)
+                assertThat(underTest.isTileToggleable()).isTrue()
+            }
+        }
+    }
+
     private companion object {
 
         val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
-        val TEST_TILE_SPEC = TileSpec.create(TEST_COMPONENT)
         val TEST_USER = UserHandle.of(1)!!
         val TEST_TILE =
             Tile().apply {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
new file mode 100644
index 0000000..f3c3579
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.inversion.domain
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tileimpl.SubtitleArrayMapping
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.impl.inversion.qsColorInversionTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ColorInversionTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val colorInversionTileConfig = kosmos.qsColorInversionTileConfig
+    private val subtitleArrayId =
+        SubtitleArrayMapping.getSubtitleId(colorInversionTileConfig.tileSpec.spec)
+    private val subtitleArray = context.resources.getStringArray(subtitleArrayId)
+    // Using lazy (versus =) to make sure we override the right context -- see b/311612168
+    private val mapper by lazy {
+        ColorInversionTileMapper(
+            context.orCreateTestableResources
+                .apply {
+                    addOverride(R.drawable.qs_invert_colors_icon_off, TestStubDrawable())
+                    addOverride(R.drawable.qs_invert_colors_icon_on, TestStubDrawable())
+                }
+                .resources,
+            context.theme
+        )
+    }
+
+    @Test
+    fun disabledModel() {
+        val inputModel = ColorInversionTileModel(false)
+
+        val outputState = mapper.map(colorInversionTileConfig, inputModel)
+
+        val expectedState =
+            createColorInversionTileState(
+                QSTileState.ActivationState.INACTIVE,
+                subtitleArray[1],
+                R.drawable.qs_invert_colors_icon_off
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel() {
+        val inputModel = ColorInversionTileModel(true)
+
+        val outputState = mapper.map(colorInversionTileConfig, inputModel)
+
+        val expectedState =
+            createColorInversionTileState(
+                QSTileState.ActivationState.ACTIVE,
+                subtitleArray[2],
+                R.drawable.qs_invert_colors_icon_on
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createColorInversionTileState(
+        activationState: QSTileState.ActivationState,
+        secondaryLabel: String,
+        iconRes: Int,
+    ): QSTileState {
+        val label = context.getString(R.string.quick_settings_inversion_label)
+        return QSTileState(
+            { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            label,
+            activationState,
+            secondaryLabel,
+            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            label,
+            null,
+            QSTileState.SideViewIcon.None,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt
new file mode 100644
index 0000000..24c7bfb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractorTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.inversion.domain.interactor
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.FakeColorInversionRepository
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ColorInversionTileDataInteractorTest : SysuiTestCase() {
+
+    private val colorInversionRepository = FakeColorInversionRepository()
+    private val underTest: ColorInversionTileDataInteractor =
+        ColorInversionTileDataInteractor(colorInversionRepository)
+
+    @Test
+    fun alwaysAvailable() = runTest {
+        val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+
+        assertThat(availability).hasSize(1)
+        assertThat(availability.last()).isTrue()
+    }
+
+    @Test
+    fun dataMatchesTheRepository() = runTest {
+        val dataList: List<ColorInversionTileModel> by
+            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
+        runCurrent()
+
+        colorInversionRepository.setIsEnabled(true, TEST_USER)
+        runCurrent()
+
+        colorInversionRepository.setIsEnabled(false, TEST_USER)
+        runCurrent()
+
+        assertThat(dataList).hasSize(3)
+        assertThat(dataList.map { it.isEnabled }).isEqualTo(listOf(false, true, false))
+    }
+
+    private companion object {
+        val TEST_USER = UserHandle.of(1)!!
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt
new file mode 100644
index 0000000..99bae18
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractorTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.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.inversion.domain.interactor
+
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.FakeColorInversionRepository
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ColorInversionUserActionInteractorTest : SysuiTestCase() {
+
+    private val testUser = UserHandle.CURRENT
+    private val repository = FakeColorInversionRepository()
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+
+    private val underTest =
+        ColorInversionUserActionInteractor(
+            repository,
+            inputHandler,
+        )
+
+    @Test
+    fun handleClickWhenEnabled() = runTest {
+        val wasEnabled = true
+        repository.setIsEnabled(wasEnabled, testUser)
+
+        underTest.handleInput(QSTileInputTestKtx.click(ColorInversionTileModel(wasEnabled)))
+
+        assertThat(repository.isEnabled(testUser).value).isEqualTo(!wasEnabled)
+    }
+
+    @Test
+    fun handleClickWhenDisabled() = runTest {
+        val wasEnabled = false
+        repository.setIsEnabled(wasEnabled, testUser)
+
+        underTest.handleInput(QSTileInputTestKtx.click(ColorInversionTileModel(wasEnabled)))
+
+        assertThat(repository.isEnabled(testUser).value).isEqualTo(!wasEnabled)
+    }
+
+    @Test
+    fun handleLongClickWhenDisabled() = runTest {
+        val enabled = false
+
+        underTest.handleInput(QSTileInputTestKtx.longClick(ColorInversionTileModel(enabled)))
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            assertThat(it.intent.action).isEqualTo(Settings.ACTION_COLOR_INVERSION_SETTINGS)
+        }
+    }
+
+    @Test
+    fun handleLongClickWhenEnabled() = runTest {
+        val enabled = true
+
+        underTest.handleInput(QSTileInputTestKtx.longClick(ColorInversionTileModel(enabled)))
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            assertThat(it.intent.action).isEqualTo(Settings.ACTION_COLOR_INVERSION_SETTINGS)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index c110de9..224903f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -265,8 +265,8 @@
                 falsingCollector = utils.falsingCollector(),
                 powerInteractor = powerInteractor,
                 bouncerInteractor = bouncerInteractor,
-                simBouncerInteractor = utils.simBouncerInteractor,
-                authenticationInteractor = utils.authenticationInteractor()
+                simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor },
+                authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() }
             )
         startable.start()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 61d55f0..2e4986d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -90,8 +90,8 @@
             falsingCollector = falsingCollector,
             powerInteractor = powerInteractor,
             bouncerInteractor = bouncerInteractor,
-            simBouncerInteractor = utils.simBouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
+            simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor },
+            authenticationInteractor = dagger.Lazy { authenticationInteractor },
         )
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenAndDreamTargetFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenAndDreamTargetFilterTest.kt
deleted file mode 100644
index 886c61a..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenAndDreamTargetFilterTest.kt
+++ /dev/null
@@ -1,149 +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.smartspace
-
-import android.app.smartspace.SmartspaceTarget
-import android.content.ContentResolver
-import android.database.ContentObserver
-import android.net.Uri
-import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.smartspace.filters.LockscreenAndDreamTargetFilter
-import com.android.systemui.util.concurrency.Execution
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.withArgCaptor
-import com.android.systemui.util.settings.SecureSettings
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.Executor
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.atLeast
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidJUnit4::class)
-class LockscreenAndDreamTargetFilterTest : SysuiTestCase() {
-    @Mock
-    private lateinit var secureSettings: SecureSettings
-
-    @Mock
-    private lateinit var userTracker: UserTracker
-
-    @Mock
-    private lateinit var execution: Execution
-
-    @Mock
-    private lateinit var handler: Handler
-
-    @Mock
-    private lateinit var contentResolver: ContentResolver
-
-    @Mock
-    private lateinit var uiExecution: Executor
-
-    @Mock
-    private lateinit var userHandle: UserHandle
-
-    @Mock
-    private lateinit var listener: SmartspaceTargetFilter.Listener
-
-    @Mock
-    private lateinit var lockScreenAllowPrivateNotificationsUri: Uri
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        `when`(userTracker.userHandle).thenReturn(userHandle)
-        `when`(secureSettings
-                .getUriFor(eq(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS)))
-                .thenReturn(lockScreenAllowPrivateNotificationsUri)
-    }
-
-    /**
-     * Ensures {@link Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS} is
-     * tracked.
-     */
-    @Test
-    fun testLockscreenAllowPrivateNotifications() {
-        var setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
-        `when`(secureSettings
-                .getIntForUser(eq(setting) ?: setting, anyInt(), anyInt()))
-                .thenReturn(0)
-        var filter = LockscreenAndDreamTargetFilter(secureSettings, userTracker, execution, handler,
-                contentResolver, uiExecution)
-
-        filter.addListener(listener)
-        var smartspaceTarget = mock(SmartspaceTarget::class.java)
-        `when`(smartspaceTarget.userHandle).thenReturn(userHandle)
-        `when`(smartspaceTarget.isSensitive).thenReturn(true)
-        assertThat(filter.filterSmartspaceTarget(smartspaceTarget)).isFalse()
-
-        var settingCaptor = ArgumentCaptor.forClass(ContentObserver::class.java)
-
-        verify(contentResolver).registerContentObserver(eq(lockScreenAllowPrivateNotificationsUri),
-                anyBoolean(), settingCaptor.capture(), anyInt())
-
-        `when`(secureSettings
-                .getIntForUser(eq(setting) ?: setting, anyInt(), anyInt()))
-                .thenReturn(1)
-
-        clearInvocations(listener)
-        settingCaptor.value.onChange(false, mock(Uri::class.java))
-        verify(listener, atLeast(1)).onCriteriaChanged()
-        assertThat(filter.filterSmartspaceTarget(smartspaceTarget)).isTrue()
-    }
-
-    /**
-     * Ensures user switches are tracked.
-     */
-    @Test
-    fun testUserSwitchCallback() {
-        var setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
-        `when`(secureSettings
-                .getIntForUser(eq(setting) ?: setting, anyInt(), anyInt()))
-                .thenReturn(0)
-        var filter = LockscreenAndDreamTargetFilter(secureSettings, userTracker, execution, handler,
-                contentResolver, uiExecution)
-
-        filter.addListener(listener)
-
-        var userTrackerCallback = withArgCaptor<UserTracker.Callback> {
-            verify(userTracker).addCallback(capture(), any())
-        }
-
-        clearInvocations(listener)
-        userTrackerCallback.onUserChanged(0, context)
-
-        verify(listener).onCriteriaChanged()
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
new file mode 100644
index 0000000..bf33010
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.smartspace
+
+import android.app.smartspace.SmartspaceTarget
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.smartspace.filters.LockscreenTargetFilter
+import com.android.systemui.util.concurrency.Execution
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.SecureSettings
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.atLeast
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
+class LockscreenTargetFilterTest : SysuiTestCase() {
+    @Mock private lateinit var secureSettings: SecureSettings
+
+    @Mock private lateinit var userTracker: UserTracker
+
+    @Mock private lateinit var execution: Execution
+
+    @Mock private lateinit var handler: Handler
+
+    @Mock private lateinit var contentResolver: ContentResolver
+
+    @Mock private lateinit var uiExecution: Executor
+
+    @Mock private lateinit var userHandle: UserHandle
+
+    @Mock private lateinit var listener: SmartspaceTargetFilter.Listener
+
+    @Mock private lateinit var lockScreenAllowPrivateNotificationsUri: Uri
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(userTracker.userHandle).thenReturn(userHandle)
+        `when`(
+                secureSettings.getUriFor(
+                    eq(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS)
+                )
+            )
+            .thenReturn(lockScreenAllowPrivateNotificationsUri)
+    }
+
+    /** Ensures {@link Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS} is tracked. */
+    @Test
+    fun testLockscreenAllowPrivateNotifications() {
+        var setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
+        `when`(secureSettings.getIntForUser(eq(setting) ?: setting, anyInt(), anyInt()))
+            .thenReturn(0)
+        var filter =
+            LockscreenTargetFilter(
+                secureSettings,
+                userTracker,
+                execution,
+                handler,
+                contentResolver,
+                uiExecution
+            )
+
+        filter.addListener(listener)
+        var smartspaceTarget = mock(SmartspaceTarget::class.java)
+        `when`(smartspaceTarget.userHandle).thenReturn(userHandle)
+        `when`(smartspaceTarget.isSensitive).thenReturn(true)
+        assertThat(filter.filterSmartspaceTarget(smartspaceTarget)).isFalse()
+
+        var settingCaptor = ArgumentCaptor.forClass(ContentObserver::class.java)
+
+        verify(contentResolver)
+            .registerContentObserver(
+                eq(lockScreenAllowPrivateNotificationsUri),
+                anyBoolean(),
+                settingCaptor.capture(),
+                anyInt()
+            )
+
+        `when`(secureSettings.getIntForUser(eq(setting) ?: setting, anyInt(), anyInt()))
+            .thenReturn(1)
+
+        clearInvocations(listener)
+        settingCaptor.value.onChange(false, mock(Uri::class.java))
+        verify(listener, atLeast(1)).onCriteriaChanged()
+        assertThat(filter.filterSmartspaceTarget(smartspaceTarget)).isTrue()
+    }
+
+    /** Ensures user switches are tracked. */
+    @Test
+    fun testUserSwitchCallback() {
+        var setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
+        `when`(secureSettings.getIntForUser(eq(setting) ?: setting, anyInt(), anyInt()))
+            .thenReturn(0)
+        var filter =
+            LockscreenTargetFilter(
+                secureSettings,
+                userTracker,
+                execution,
+                handler,
+                contentResolver,
+                uiExecution
+            )
+
+        filter.addListener(listener)
+
+        var userTrackerCallback =
+            withArgCaptor<UserTracker.Callback> {
+                verify(userTracker).addCallback(capture(), any())
+            }
+
+        clearInvocations(listener)
+        userTrackerCallback.onUserChanged(0, context)
+
+        verify(listener).onCriteriaChanged()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationContentDescriptionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationContentDescriptionTest.kt
new file mode 100644
index 0000000..f67c70c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationContentDescriptionTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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
+
+import android.app.Notification
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationContentDescriptionTest : SysuiTestCase() {
+
+    private val TITLE = "this is a title"
+    private val TEXT = "this is text"
+    private val TICKER = "this is a ticker"
+
+    @Test
+    fun notificationWithAllDifferentFields_descriptionIsTitle() {
+        val n = createNotification(TITLE, TEXT, TICKER)
+        val description = contentDescForNotification(context, n)
+        assertThat(description).isEqualTo(createDescriptionText(n, TITLE))
+    }
+
+    @Test
+    fun notificationWithAllDifferentFields_titleMatchesAppName_descriptionIsText() {
+        val n = createNotification(getTestAppName(), TEXT, TICKER)
+        val description = contentDescForNotification(context, n)
+        assertThat(description).isEqualTo(createDescriptionText(n, TEXT))
+    }
+
+    @Test
+    fun notificationWithAllDifferentFields_titleMatchesAppNameNoText_descriptionIsTicker() {
+        val n = createNotification(getTestAppName(), null, TICKER)
+        val description = contentDescForNotification(context, n)
+        assertThat(description).isEqualTo(createDescriptionText(n, TICKER))
+    }
+
+    @Test
+    fun notificationWithAllDifferentFields_titleMatchesAppNameNoTextNoTicker_descriptionEmpty() {
+        val appName = getTestAppName()
+        val n = createNotification(appName, null, null)
+        val description = contentDescForNotification(context, n)
+        assertThat(description).isEqualTo(createDescriptionText(n, ""))
+    }
+
+    @Test
+    fun nullNotification_descriptionIsAppName() {
+        val description = contentDescForNotification(context, null)
+        assertThat(description).isEqualTo(createDescriptionText(null, ""))
+    }
+
+    private fun createNotification(
+        title: String? = null,
+        text: String? = null,
+        ticker: String? = null
+    ): Notification =
+        Notification.Builder(context)
+            .setContentTitle(title)
+            .setContentText(text)
+            .setTicker(ticker)
+            .build()
+
+    private fun getTestAppName(): String {
+        return getAppName(createNotification("", "", ""))
+    }
+
+    private fun getAppName(n: Notification?) =
+        n?.let {
+            val builder = Notification.Builder.recoverBuilder(context, it)
+            builder.loadHeaderAppName()
+        }
+            ?: ""
+
+    private fun createDescriptionText(n: Notification?, desc: String?): String {
+        val appName = getAppName(n)
+        return context.getString(R.string.accessibility_desc_notification_icon, appName, desc)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 791a028..1237347 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -16,9 +16,12 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION
+import com.android.systemui.Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.coroutines.collectLastValue
@@ -183,7 +186,8 @@
         }
 
     @Test
-    fun activity_nullSsid_outputsFalse() =
+    @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+    fun activity_nullSsid_outputsFalse_staticFlagOff() =
         testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
@@ -207,6 +211,33 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+    fun activity_nullSsid_outputsFalse_staticFlagOn() =
+        testScope.runTest {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.Active(NETWORK_ID, ssid = null, level = 1)
+            )
+
+            val activityIn by collectLastValue(underTest.isActivityInViewVisible)
+            val activityOut by collectLastValue(underTest.isActivityOutViewVisible)
+            val activityContainer by collectLastValue(underTest.isActivityContainerVisible)
+
+            // WHEN we update the repo to have activity
+            val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+            wifiRepository.setWifiActivity(activity)
+
+            // THEN we still output false because our network's SSID is null
+            assertThat(activityIn).isFalse()
+            assertThat(activityOut).isFalse()
+
+            // THEN the inout indicators are sill showing due to the config being true
+            assertThat(activityContainer).isTrue()
+        }
+
+    @Test
     fun activity_allLocationViewModelsReceiveSameData() =
         testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
@@ -335,7 +366,8 @@
         }
 
     @Test
-    fun activityContainer_inAndOutFalse_outputsFalse() =
+    @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+    fun activityContainer_inAndOutFalse_outputsTrue_staticFlagOff() =
         testScope.runTest {
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
@@ -350,6 +382,24 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+    fun activityContainer_inAndOutFalse_outputsTrue_staticFlagOn() =
+        testScope.runTest {
+            whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+            wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+
+            val latest by collectLastValue(underTest.isActivityContainerVisible)
+
+            val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+            wifiRepository.setWifiActivity(activity)
+
+            // The activity container should always be visible, since activity is
+            // shown in UI by changing opacity of the indicators.
+            assertThat(latest).isTrue()
+        }
+
+    @Test
     fun airplaneSpacer_notAirplaneMode_outputsFalse() =
         testScope.runTest {
             val latest by collectLastValue(underTest.isAirplaneSpacerVisible)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 1c5f221..4436be7 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -41,16 +41,13 @@
 
 /** Interface for building clocks and providing information about those clocks */
 interface ClockProvider {
+    /** Initializes the clock provider with debug log buffers */
+    fun initialize(buffers: ClockMessageBuffers?)
+
     /** Returns metadata for all clocks this provider knows about */
     fun getClocks(): List<ClockMetadata>
 
     /** Initializes and returns the target clock design */
-    @Deprecated("Use overload with ClockSettings")
-    fun createClock(id: ClockId): ClockController {
-        return createClock(ClockSettings(id, null))
-    }
-
-    /** Initializes and returns the target clock design */
     fun createClock(settings: ClockSettings): ClockController
 
     /** A static thumbnail for rendering in some examples */
@@ -98,11 +95,20 @@
 
     /** Triggers for various animations */
     val animations: ClockAnimations
-
-    /** Some clocks may log debug information */
-    var messageBuffer: MessageBuffer?
 }
 
+/** For clocks that want to report debug information */
+data class ClockMessageBuffers(
+    /** Message buffer for general infra */
+    val infraMessageBuffer: MessageBuffer,
+
+    /** Message buffer for small clock renering */
+    val smallClockMessageBuffer: MessageBuffer,
+
+    /** Message buffer for large clock rendering */
+    val largeClockMessageBuffer: MessageBuffer,
+)
+
 /** Specifies layout information for the */
 interface ClockFaceLayout {
     /** All clock views to add to the root constraint layout before applying constraints. */
diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags
index 21b019e..f9546c4 100644
--- a/packages/SystemUI/proguard_common.flags
+++ b/packages/SystemUI/proguard_common.flags
@@ -38,6 +38,11 @@
 -keepnames class com.android.internal.protolog.** { *; }
 -keepnames class android.hardware.common.** { *; }
 
+# TODO(b/316553881): Statically linking flags into SystemUI and bootclasspath might not be a
+# good idea in the first place
+-keepnames class com.android.window.flags.Flags { public *; }
+
+
 # Allows proguard to make private and protected methods and fields public as
 # part of optimization. This lets proguard inline trivial getter/setter methods.
 -allowaccessmodification
diff --git a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
index 7b90270..50d5607 100644
--- a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
+++ b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
@@ -15,7 +15,7 @@
   ~
   -->
 
-<FrameLayout
+<androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/alternate_bouncer"
@@ -32,4 +32,4 @@
         android:importantForAccessibility="no"
         sysui:ignoreRightInset="true"
     />
-</FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
index a8017f6..25cadfb 100644
--- a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
+++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
@@ -39,7 +39,6 @@
                 android:adjustViewBounds="true"
                 android:layout_width="wrap_content"
                 android:src="@drawable/ic_activity_down"
-                android:visibility="gone"
                 android:paddingEnd="2dp"
                 />
             <ImageView
@@ -49,7 +48,6 @@
                 android:layout_width="wrap_content"
                 android:src="@drawable/ic_activity_up"
                 android:paddingEnd="2dp"
-                android:visibility="gone"
                 />
         </FrameLayout>
         <FrameLayout
diff --git a/packages/SystemUI/res/drawable/ic_memory.xml b/packages/SystemUI/res/drawable/ic_memory.xml
deleted file mode 100644
index ada36c5..0000000
--- a/packages/SystemUI/res/drawable/ic_memory.xml
+++ /dev/null
@@ -1,28 +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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
-        android:fillAlpha="0.5"
-        android:fillColor="#000000"/>
-    <path
-        android:pathData="M6,9 L6,7 L4,7 L4,5 L6,5 C6,3.9 6.9,3 8,3 L16,3 C17.1,3 18,3.9 18,5 L20,5 L20,7 L18,7 L18,9 L20,9 L20,11 L18,11 L18,13 L20,13 L20,15 L18,15 L18,17 L20,17 L20,19 L18,19 C18,20.1 17.1,21 16,21 L8,21 C6.9,21 6,20.1 6,19 L4,19 L4,17 L6,17 L6,15 L4,15 L4,13 L6,13 L6,11 L4,11 L4,9 L6,9 Z M16,19 L16,5 L8,5 L8,19 L16,19 Z"
-        android:fillColor="#000000"/>
-</vector>
diff --git a/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
index 4c5cd7d..e8b8922 100644
--- a/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
+++ b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
@@ -37,7 +37,6 @@
                 android:adjustViewBounds="true"
                 android:layout_width="wrap_content"
                 android:src="@drawable/ic_activity_down"
-                android:visibility="gone"
                 android:paddingEnd="2dp"
             />
             <ImageView
@@ -47,7 +46,6 @@
                 android:layout_width="wrap_content"
                 android:src="@drawable/ic_activity_up"
                 android:paddingEnd="2dp"
-                android:visibility="gone"
             />
         </FrameLayout>
         <FrameLayout
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index ca0fb85..dca84b9 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -26,6 +26,12 @@
     android:layout_height="match_parent"
     android:fitsSystemWindows="true">
 
+    <!-- Placeholder for the communal UI that will be replaced if the feature is enabled. -->
+    <ViewStub
+        android:id="@+id/communal_ui_stub"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
     <com.android.systemui.scrim.ScrimView
         android:id="@+id/scrim_behind"
         android:layout_width="match_parent"
@@ -64,12 +70,6 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
-    <!-- Placeholder for the communal UI that will be replaced if the feature is enabled. -->
-    <ViewStub
-        android:id="@+id/communal_ui_stub"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
     <include layout="@layout/brightness_mirror_container" />
 
     <com.android.systemui.scrim.ScrimView
@@ -116,6 +116,10 @@
         android:inflatedId="@+id/multi_shade"
         android:layout="@layout/multi_shade" />
 
+    <include layout="@layout/alternate_bouncer"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
     <com.android.systemui.biometrics.AuthRippleView
         android:id="@+id/auth_ripple"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/widget_picker.xml b/packages/SystemUI/res/layout/widget_picker.xml
deleted file mode 100644
index 827bd5d..0000000
--- a/packages/SystemUI/res/layout/widget_picker.xml
+++ /dev/null
@@ -1,26 +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.
-  -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/widgets_container"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:padding="64dp"
-    android:gravity="center_vertical"
-    android:orientation="horizontal">
-
-</LinearLayout>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 33a0a06..e01a2aa 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -480,9 +480,6 @@
      This name is in the ComponentName flattened format (package/class)  -->
     <string name="config_remoteCopyPackage" translatable="false"></string>
 
-    <!-- On debuggable builds, alert the user if SystemUI PSS goes over this number (in kb) -->
-    <integer name="watch_heap_limit">256000</integer>
-
     <!-- SystemUI Plugins that can be loaded on user builds. -->
     <string-array name="config_pluginAllowlist" translatable="false">
         <item>com.android.systemui</item>
@@ -543,12 +540,16 @@
 
     <!--  ID for the camera of outer display that needs extra protection -->
     <string translatable="false" name="config_protectedCameraId"></string>
+    <!--  Physical ID for the camera of outer display that needs extra protection -->
+    <string translatable="false" name="config_protectedPhysicalCameraId"></string>
 
     <!-- Similar to config_frontBuiltInDisplayCutoutProtection but for inner display. -->
     <string translatable="false" name="config_innerBuiltInDisplayCutoutProtection"></string>
 
     <!-- ID for the camera of inner display that needs extra protection -->
     <string translatable="false" name="config_protectedInnerCameraId"></string>
+    <!-- Physical ID for the camera of inner display that needs extra protection -->
+    <string translatable="false" name="config_protectedInnerPhysicalCameraId"></string>
 
     <!-- Comma-separated list of packages to exclude from camera protection e.g.
     "com.android.systemui,com.android.xyz" -->
@@ -962,14 +963,6 @@
     <bool name="config_edgeToEdgeBottomSheetDialog">true</bool>
 
     <!--
-    Whether the scene container framework is enabled.
-
-    The scene container framework is a newer (2023) way to organize the various "scenes" between the
-    bouncer, lockscreen, shade, and quick settings.
-    -->
-    <bool name="config_sceneContainerFrameworkEnabled">true</bool>
-
-    <!--
     Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate
     TODO(b/302332976) Get this value from the HAL if they can provide an API for it.
     -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index b947801..ee89ede 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -274,6 +274,10 @@
     <!-- Side padding on the side of notifications -->
     <dimen name="notification_side_paddings">16dp</dimen>
 
+    <!-- Starting translateY offset of the HUN appear and disappear animations. Indicates
+    the amount by the view is positioned above the screen before the animation starts. -->
+    <dimen name="heads_up_appear_y_above_screen">32dp</dimen>
+
     <!-- padding between the heads up and the statusbar -->
     <dimen name="heads_up_status_bar_padding">8dp</dimen>
 
@@ -1905,4 +1909,7 @@
     <!-- Bouncer user switcher margins -->
     <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
     <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
+
+    <!-- UDFPS view attributes -->
+    <dimen name="udfps_icon_size">6mm</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index cf63cc7..d511cab 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -259,4 +259,11 @@
     <item type="id" name="device_entry_icon_view" />
     <item type="id" name="device_entry_icon_fg" />
     <item type="id" name="device_entry_icon_bg" />
+
+    <!--Id for the device-entry UDFPS icon that lives in the alternate bouncer. -->
+    <item type="id" name="alternate_bouncer_udfps_icon_view" />
+
+    <!-- Id for the udfps accessibility overlay -->
+    <item type="id" name="udfps_accessibility_overlay" />
+    <item type="id" name="udfps_accessibility_overlay_top_guideline" />
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e10925d..f4b25a7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2397,10 +2397,6 @@
     <!-- URl of the webpage that explains battery saver. -->
     <string name="help_uri_battery_saver_learn_more_link_target" translatable="false"></string>
 
-    <!-- Name for a quick settings tile, used only by platform developers, to extract the SystemUI process memory and send it to another
-         app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
-    <string name="heap_dump_tile_name">Dump SysUI Heap</string>
-
     <!-- Title for the privacy indicators dialog, only appears as part of a11y descriptions [CHAR LIMIT=NONE] -->
     <string name="ongoing_privacy_dialog_a11y_title">In use</string>
 
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 5b59e7d..2b41178 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -34,6 +34,9 @@
     srcs: [
         ":statslog-SystemUI-java-gen",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 android_library {
@@ -70,6 +73,9 @@
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
     kotlincflags: ["-Xjvm-default=all"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -81,6 +87,9 @@
     static_kotlin_stdlib: false,
     java_version: "1.8",
     min_sdk_version: "current",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -100,4 +109,7 @@
     },
     java_version: "1.8",
     min_sdk_version: "current",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
index 8ad32b4..a00cdfa 100644
--- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
@@ -30,6 +30,10 @@
     fun isUdfps(): Boolean {
         return (this == UDFPS_OPTICAL) || (this == UDFPS_ULTRASONIC)
     }
+
+    fun isPowerButton(): Boolean {
+        return this == POWER_BUTTON
+    }
 }
 
 /** Convert [this] to corresponding [FingerprintSensorType] */
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index d8c1e41..131eb6b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -367,7 +367,7 @@
     /**
      * Corner radius that should be used on windows in order to cover the display.
      * These values are expressed in pixels because they should not respect display or font
-     * scaling, this means that we don't have to reload them on config changes.
+     * scaling. The corner radius may change when folding/unfolding the device.
      */
     public static float getWindowCornerRadius(Context context) {
         return ScreenDecorationsUtils.getWindowCornerRadius(context);
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 76abad8..bcc2044 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -45,12 +45,11 @@
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
 import com.android.systemui.log.core.LogLevel.DEBUG
-import com.android.systemui.log.dagger.KeyguardLargeClockLog
-import com.android.systemui.log.dagger.KeyguardSmallClockLog
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFaceController
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockTickRate
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.WeatherData
@@ -91,117 +90,120 @@
     private val context: Context,
     @Main private val mainExecutor: DelayableExecutor,
     @Background private val bgExecutor: Executor,
-    @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
-    @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
+    private val clockBuffers: ClockMessageBuffers,
     private val featureFlags: FeatureFlags,
     private val zenModeController: ZenModeController,
 ) {
+    var loggers = listOf(
+        clockBuffers.infraMessageBuffer,
+        clockBuffers.smallClockMessageBuffer,
+        clockBuffers.largeClockMessageBuffer
+    ).map { Logger(it, TAG) }
+
     var clock: ClockController? = null
+        get() = field
         set(value) {
-            smallClockOnAttachStateChangeListener?.let {
-                field?.smallClock?.view?.removeOnAttachStateChangeListener(it)
+            disconnectClock(field)
+            field = value
+            connectClock(value)
+        }
+
+    private fun disconnectClock(clock: ClockController?) {
+        if (clock == null) { return; }
+        smallClockOnAttachStateChangeListener?.let {
+            clock.smallClock.view.removeOnAttachStateChangeListener(it)
+            smallClockFrame?.viewTreeObserver
+                    ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
+        }
+        largeClockOnAttachStateChangeListener?.let {
+            clock.largeClock.view.removeOnAttachStateChangeListener(it)
+        }
+    }
+
+    private fun connectClock(clock: ClockController?) {
+        if (clock == null) { return; }
+        val clockStr = clock.toString()
+        loggers.forEach { it.d({ "New Clock: $str1" }) { str1 = clockStr } }
+
+        clock.initialize(resources, dozeAmount, 0f)
+
+        if (!regionSamplingEnabled) {
+            updateColors()
+        } else {
+            smallRegionSampler = createRegionSampler(
+                clock.smallClock.view,
+                mainExecutor,
+                bgExecutor,
+                regionSamplingEnabled,
+                isLockscreen = true,
+                ::updateColors
+            ).apply { startRegionSampler() }
+
+            largeRegionSampler = createRegionSampler(
+                clock.largeClock.view,
+                mainExecutor,
+                bgExecutor,
+                regionSamplingEnabled,
+                isLockscreen = true,
+                ::updateColors
+            ).apply { startRegionSampler() }
+
+            updateColors()
+        }
+        updateFontSizes()
+        updateTimeListeners()
+
+        weatherData?.let {
+            if (WeatherData.DEBUG) {
+                Log.i(TAG, "Pushing cached weather data to new clock: $it")
+            }
+            clock.events.onWeatherDataChanged(it)
+        }
+        zenData?.let {
+            clock.events.onZenDataChanged(it)
+        }
+        alarmData?.let {
+            clock.events.onAlarmDataChanged(it)
+        }
+
+        smallClockOnAttachStateChangeListener = object : OnAttachStateChangeListener {
+            var pastVisibility: Int? = null
+            override fun onViewAttachedToWindow(view: View) {
+                clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                // Match the asing for view.parent's layout classes.
+                smallClockFrame = (view.parent as ViewGroup)?.also { frame ->
+                    pastVisibility = frame.visibility
+                    onGlobalLayoutListener = OnGlobalLayoutListener {
+                        val currentVisibility = frame.visibility
+                        if (pastVisibility != currentVisibility) {
+                            pastVisibility = currentVisibility
+                            // when small clock is visible,
+                            // recalculate bounds and sample
+                            if (currentVisibility == View.VISIBLE) {
+                                smallRegionSampler?.stopRegionSampler()
+                                smallRegionSampler?.startRegionSampler()
+                            }
+                        }
+                    }
+                    frame.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
+                }
+            }
+
+            override fun onViewDetachedFromWindow(p0: View) {
                 smallClockFrame?.viewTreeObserver
                         ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
             }
-            largeClockOnAttachStateChangeListener?.let {
-                field?.largeClock?.view?.removeOnAttachStateChangeListener(it)
-            }
-
-            field = value
-            if (value != null) {
-                smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
-                value.smallClock.messageBuffer = smallLogBuffer
-                largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
-                value.largeClock.messageBuffer = largeLogBuffer
-
-                value.initialize(resources, dozeAmount, 0f)
-
-                if (!regionSamplingEnabled) {
-                    updateColors()
-                } else {
-                    clock?.let {
-                        smallRegionSampler = createRegionSampler(
-                                it.smallClock.view,
-                                mainExecutor,
-                                bgExecutor,
-                                regionSamplingEnabled,
-                                isLockscreen = true,
-                                ::updateColors
-                        )?.apply { startRegionSampler() }
-
-                        largeRegionSampler = createRegionSampler(
-                                it.largeClock.view,
-                                mainExecutor,
-                                bgExecutor,
-                                regionSamplingEnabled,
-                                isLockscreen = true,
-                                ::updateColors
-                        )?.apply { startRegionSampler() }
-
-                        updateColors()
-                    }
-                }
-                updateFontSizes()
-                updateTimeListeners()
-                weatherData?.let {
-                    if (WeatherData.DEBUG) {
-                        Log.i(TAG, "Pushing cached weather data to new clock: $it")
-                    }
-                    value.events.onWeatherDataChanged(it)
-                }
-                zenData?.let {
-                    value.events.onZenDataChanged(it)
-                }
-                alarmData?.let {
-                    value.events.onAlarmDataChanged(it)
-                }
-
-                smallClockOnAttachStateChangeListener =
-                    object : OnAttachStateChangeListener {
-                        var pastVisibility: Int? = null
-                        override fun onViewAttachedToWindow(view: View) {
-                            value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
-                            // Match the asing for view.parent's layout classes.
-                            smallClockFrame = view.parent as ViewGroup
-                            smallClockFrame?.let { frame ->
-                                pastVisibility = frame.visibility
-                                onGlobalLayoutListener = OnGlobalLayoutListener {
-                                    val currentVisibility = frame.visibility
-                                    if (pastVisibility != currentVisibility) {
-                                        pastVisibility = currentVisibility
-                                        // when small clock is visible,
-                                        // recalculate bounds and sample
-                                        if (currentVisibility == View.VISIBLE) {
-                                            smallRegionSampler?.stopRegionSampler()
-                                            smallRegionSampler?.startRegionSampler()
-                                        }
-                                    }
-                                }
-                                frame.viewTreeObserver
-                                        .addOnGlobalLayoutListener(onGlobalLayoutListener)
-                            }
-                        }
-
-                        override fun onViewDetachedFromWindow(p0: View) {
-                            smallClockFrame?.viewTreeObserver
-                                    ?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
-                        }
-                }
-                value.smallClock.view
-                        .addOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
-
-                largeClockOnAttachStateChangeListener =
-                    object : OnAttachStateChangeListener {
-                        override fun onViewAttachedToWindow(p0: View) {
-                            value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
-                        }
-                        override fun onViewDetachedFromWindow(p0: View) {
-                        }
-                }
-                value.largeClock.view
-                        .addOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
-            }
         }
+        clock.smallClock.view.addOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
+
+        largeClockOnAttachStateChangeListener = object : OnAttachStateChangeListener {
+            override fun onViewAttachedToWindow(p0: View) {
+                clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+            }
+            override fun onViewDetachedFromWindow(p0: View) {}
+        }
+        clock.largeClock.view.addOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
+    }
 
     @VisibleForTesting
     var smallClockOnAttachStateChangeListener: OnAttachStateChangeListener? = null
@@ -247,6 +249,7 @@
             largeClock.events.onRegionDarknessChanged(isRegionDark)
         }
     }
+
     protected open fun createRegionSampler(
         sampledView: View,
         mainExecutor: Executor?,
@@ -254,7 +257,7 @@
         regionSamplingEnabled: Boolean,
         isLockscreen: Boolean,
         updateColors: () -> Unit
-    ): RegionSampler? {
+    ): RegionSampler {
         return RegionSampler(
             sampledView,
             mainExecutor,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index cdd7b80..74b975c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -39,7 +39,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.systemui.Dumpable;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -48,9 +47,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
-import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.KeyguardClockLog;
@@ -62,17 +59,11 @@
 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.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 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.ui.SystemBarUtilsState;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.settings.SecureSettings;
@@ -102,14 +93,7 @@
     private final DumpManager mDumpManager;
     private final ClockEventController mClockEventController;
     private final LogBuffer mLogBuffer;
-    private final NotificationIconContainerAlwaysOnDisplayViewModel mAodIconsViewModel;
-    private final KeyguardRootViewModel mKeyguardRootViewModel;
-    private final ConfigurationState mConfigurationState;
-    private final SystemBarUtilsState mSystemBarUtilsState;
-    private final DozeParameters mDozeParameters;
-    private final ScreenOffAnimationController mScreenOffAnimationController;
-    private final AlwaysOnDisplayNotificationIconViewStore mAodIconViewStore;
-    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
+    private final NotificationIconContainerAlwaysOnDisplayViewBinder mNicViewBinder;
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
@@ -183,9 +167,7 @@
             KeyguardSliceViewController keyguardSliceViewController,
             NotificationIconAreaController notificationIconAreaController,
             LockscreenSmartspaceController smartspaceController,
-            SystemBarUtilsState systemBarUtilsState,
-            ScreenOffAnimationController screenOffAnimationController,
-            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
+            NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main DelayableExecutor uiExecutor,
@@ -193,11 +175,6 @@
             DumpManager dumpManager,
             ClockEventController clockEventController,
             @KeyguardClockLog LogBuffer logBuffer,
-            NotificationIconContainerAlwaysOnDisplayViewModel aodIconsViewModel,
-            KeyguardRootViewModel keyguardRootViewModel,
-            ConfigurationState configurationState,
-            DozeParameters dozeParameters,
-            AlwaysOnDisplayNotificationIconViewStore aodIconViewStore,
             KeyguardInteractor keyguardInteractor,
             KeyguardClockInteractor keyguardClockInteractor,
             FeatureFlagsClassic featureFlags,
@@ -208,9 +185,7 @@
         mKeyguardSliceViewController = keyguardSliceViewController;
         mNotificationIconAreaController = notificationIconAreaController;
         mSmartspaceController = smartspaceController;
-        mSystemBarUtilsState = systemBarUtilsState;
-        mScreenOffAnimationController = screenOffAnimationController;
-        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
+        mNicViewBinder = nicViewBinder;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mBgExecutor = bgExecutor;
@@ -218,11 +193,6 @@
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
         mLogBuffer = logBuffer;
-        mAodIconsViewModel = aodIconsViewModel;
-        mKeyguardRootViewModel = keyguardRootViewModel;
-        mConfigurationState = configurationState;
-        mDozeParameters = dozeParameters;
-        mAodIconViewStore = aodIconViewStore;
         mView.setLogBuffer(mLogBuffer);
         mFeatureFlags = featureFlags;
         mKeyguardInteractor = keyguardInteractor;
@@ -619,28 +589,7 @@
                     mAodIconsBindHandle.dispose();
                 }
                 if (nic != null) {
-                    final DisposableHandle viewHandle =
-                            NotificationIconContainerViewBinder.bindWhileAttached(
-                                    nic,
-                                    mAodIconsViewModel,
-                                    mConfigurationState,
-                                    mSystemBarUtilsState,
-                                    mIconViewBindingFailureTracker,
-                                    mAodIconViewStore);
-                    final DisposableHandle visHandle = KeyguardRootViewBinder.bindAodIconVisibility(
-                            nic,
-                            mKeyguardRootViewModel.isNotifIconContainerVisible(),
-                            mConfigurationState,
-                            mFeatureFlags,
-                            mScreenOffAnimationController);
-                    if (visHandle == null) {
-                        mAodIconsBindHandle = viewHandle;
-                    } else {
-                        mAodIconsBindHandle = () -> {
-                            viewHandle.dispose();
-                            visHandle.dispose();
-                        };
-                    }
+                    mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic);
                     mAodIconContainer = nic;
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index 661ce2c..878a5d8 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -28,9 +28,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.dagger.KeyguardClockLog;
 import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.clocks.ClockMessageBuffers;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.shared.clocks.DefaultClockProvider;
@@ -56,7 +55,7 @@
             FeatureFlags featureFlags,
             @Main Resources resources,
             LayoutInflater layoutInflater,
-            @KeyguardClockLog LogBuffer logBuffer) {
+            ClockMessageBuffers clockBuffers) {
         ClockRegistry registry = new ClockRegistry(
                 context,
                 pluginManager,
@@ -72,7 +71,7 @@
                         featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION),
                         migrateClocksToBlueprint()),
                 context.getString(R.string.lockscreen_clock_id_fallback),
-                logBuffer,
+                clockBuffers,
                 /* keepAllLoaded = */ false,
                 /* subTag = */ "System",
                 /* isTransitClockEnabled = */ featureFlags.isEnabled(Flags.TRANSIT_CLOCK));
diff --git a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
index d33d279..5f5cca8 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
@@ -38,26 +38,75 @@
     excludedPackages: String,
     private val executor: Executor
 ) {
+    private var activeProtectionInfo: CameraProtectionInfo? = null
+    private var openCamera: OpenCameraInfo? = null
+    private val unavailablePhysicalCameras = mutableSetOf<String>()
     private val excludedPackageIds: Set<String>
     private val listeners = mutableListOf<CameraTransitionCallback>()
     private val availabilityCallback: CameraManager.AvailabilityCallback =
-            object : CameraManager.AvailabilityCallback() {
-                override fun onCameraClosed(cameraId: String) {
-                    cameraProtectionInfoList.forEach {
-                        if (cameraId == it.cameraId) {
-                            notifyCameraInactive()
-                        }
-                    }
+        object : CameraManager.AvailabilityCallback() {
+            override fun onCameraClosed(logicalCameraId: String) {
+                openCamera = null
+                if (activeProtectionInfo?.logicalCameraId == logicalCameraId) {
+                    notifyCameraInactive()
                 }
+                activeProtectionInfo = null
+            }
 
-                override fun onCameraOpened(cameraId: String, packageId: String) {
-                    cameraProtectionInfoList.forEach {
-                        if (cameraId == it.cameraId && !isExcluded(packageId)) {
-                            notifyCameraActive(it)
-                        }
-                    }
+            override fun onCameraOpened(logicalCameraId: String, packageId: String) {
+                openCamera = OpenCameraInfo(logicalCameraId, packageId)
+                if (isExcluded(packageId)) {
+                    return
                 }
-    }
+                val protectionInfo =
+                    cameraProtectionInfoList.firstOrNull {
+                        logicalCameraId == it.logicalCameraId &&
+                            it.physicalCameraId !in unavailablePhysicalCameras
+                    }
+                if (protectionInfo != null) {
+                    activeProtectionInfo = protectionInfo
+                    notifyCameraActive(protectionInfo)
+                }
+            }
+
+            override fun onPhysicalCameraAvailable(
+                logicalCameraId: String,
+                physicalCameraId: String
+            ) {
+                unavailablePhysicalCameras -= physicalCameraId
+                val openCamera = this@CameraAvailabilityListener.openCamera ?: return
+                if (openCamera.logicalCameraId != logicalCameraId) {
+                    return
+                }
+                if (isExcluded(openCamera.packageId)) {
+                    return
+                }
+                val newActiveInfo =
+                    cameraProtectionInfoList.find {
+                        it.logicalCameraId == logicalCameraId &&
+                            it.physicalCameraId == physicalCameraId
+                    }
+                if (newActiveInfo != null) {
+                    activeProtectionInfo = newActiveInfo
+                    notifyCameraActive(newActiveInfo)
+                }
+            }
+
+            override fun onPhysicalCameraUnavailable(
+                logicalCameraId: String,
+                physicalCameraId: String
+            ) {
+                unavailablePhysicalCameras += physicalCameraId
+                val activeInfo = activeProtectionInfo ?: return
+                if (
+                    activeInfo.logicalCameraId == logicalCameraId &&
+                        activeInfo.physicalCameraId == physicalCameraId
+                ) {
+                    activeProtectionInfo = null
+                    notifyCameraInactive()
+                }
+            }
+        }
 
     init {
         excludedPackageIds = excludedPackages.split(",").toSet()
@@ -106,24 +155,21 @@
         listeners.forEach { it.onHideCameraProtection() }
     }
 
-    /**
-     * Callbacks to tell a listener that a relevant camera turned on and off.
-     */
+    /** Callbacks to tell a listener that a relevant camera turned on and off. */
     interface CameraTransitionCallback {
         fun onApplyCameraProtection(protectionPath: Path, bounds: Rect)
+
         fun onHideCameraProtection()
     }
 
     companion object Factory {
         fun build(context: Context, executor: Executor): CameraAvailabilityListener {
-            val manager = context
-                    .getSystemService(Context.CAMERA_SERVICE) as CameraManager
+            val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
             val res = context.resources
             val cameraProtectionInfoList = loadCameraProtectionInfoList(res)
             val excluded = res.getString(R.string.config_cameraProtectionExcludedPackages)
 
-            return CameraAvailabilityListener(
-                    manager, cameraProtectionInfoList, excluded, executor)
+            return CameraAvailabilityListener(manager, cameraProtectionInfoList, excluded, executor)
         }
 
         private fun pathFromString(pathString: String): Path {
@@ -140,19 +186,23 @@
 
         private fun loadCameraProtectionInfoList(res: Resources): List<CameraProtectionInfo> {
             val list = mutableListOf<CameraProtectionInfo>()
-            val front = loadCameraProtectionInfo(
+            val front =
+                loadCameraProtectionInfo(
                     res,
                     R.string.config_protectedCameraId,
+                    R.string.config_protectedPhysicalCameraId,
                     R.string.config_frontBuiltInDisplayCutoutProtection
-            )
+                )
             if (front != null) {
                 list.add(front)
             }
-            val inner = loadCameraProtectionInfo(
+            val inner =
+                loadCameraProtectionInfo(
                     res,
                     R.string.config_protectedInnerCameraId,
+                    R.string.config_protectedInnerPhysicalCameraId,
                     R.string.config_innerBuiltInDisplayCutoutProtection
-            )
+                )
             if (inner != null) {
                 list.add(inner)
             }
@@ -160,30 +210,44 @@
         }
 
         private fun loadCameraProtectionInfo(
-                res: Resources,
-                cameraIdRes: Int,
-                pathRes: Int
+            res: Resources,
+            cameraIdRes: Int,
+            physicalCameraIdRes: Int,
+            pathRes: Int
         ): CameraProtectionInfo? {
-            val cameraId = res.getString(cameraIdRes)
-            if (cameraId == null || cameraId.isEmpty()) {
+            val logicalCameraId = res.getString(cameraIdRes)
+            if (logicalCameraId.isNullOrEmpty()) {
                 return null
             }
+            val physicalCameraId = res.getString(physicalCameraIdRes)
             val protectionPath = pathFromString(res.getString(pathRes))
             val computed = RectF()
             protectionPath.computeBounds(computed)
-            val protectionBounds = Rect(
+            val protectionBounds =
+                Rect(
                     computed.left.roundToInt(),
                     computed.top.roundToInt(),
                     computed.right.roundToInt(),
                     computed.bottom.roundToInt()
+                )
+            return CameraProtectionInfo(
+                logicalCameraId,
+                physicalCameraId,
+                protectionPath,
+                protectionBounds
             )
-            return CameraProtectionInfo(cameraId, protectionPath, protectionBounds)
         }
     }
 
-    data class CameraProtectionInfo (
-            val cameraId: String,
-            val cutoutProtectionPath: Path,
-            val cutoutBounds: Rect
+    data class CameraProtectionInfo(
+        val logicalCameraId: String,
+        val physicalCameraId: String?,
+        val cutoutProtectionPath: Path,
+        val cutoutBounds: Rect,
+    )
+
+    private data class OpenCameraInfo(
+        val logicalCameraId: String,
+        val packageId: String,
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index c07a4d2..7295936 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui;
 
-import android.content.res.Configuration;
-
 import androidx.annotation.NonNull;
 
 import java.io.PrintWriter;
@@ -42,13 +40,6 @@
     /** Main entry point for implementations. Called shortly after SysUI startup. */
     void start();
 
-    /** Called when the device configuration changes. This will not be called before
-     * {@link #start()}, but it could be called before {@link #onBootCompleted()}.
-     *
-     * @see android.app.Application#onConfigurationChanged(Configuration)  */
-    default void onConfigurationChanged(Configuration newConfig) {
-    }
-
     @Override
     default void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
     }
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 6faee8c..e03c627 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -89,6 +89,7 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.ThreadFactory;
 import com.android.systemui.util.settings.SecureSettings;
@@ -109,7 +110,8 @@
  * for antialiasing and emulation purposes.
  */
 @SysUISingleton
-public class ScreenDecorations implements CoreStartable, Dumpable {
+public class ScreenDecorations implements
+        CoreStartable, ConfigurationController.ConfigurationListener, Dumpable {
     private static final boolean DEBUG_LOGGING = false;
     private static final String TAG = "ScreenDecorations";
 
@@ -179,6 +181,7 @@
     @VisibleForTesting
     protected DisplayInfo mDisplayInfo = new DisplayInfo();
     private DisplayCutout mDisplayCutout;
+    private boolean mPendingManualConfigUpdate;
 
     @VisibleForTesting
     protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
@@ -571,6 +574,11 @@
                         setupDecorations();
                         return;
                     }
+
+                    if (mPendingManualConfigUpdate) {
+                        mPendingManualConfigUpdate = false;
+                        onConfigChanged(mContext.getResources().getConfiguration());
+                    }
                 }
             }
         };
@@ -1056,7 +1064,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
             Log.i(TAG, "ScreenDecorations is disabled");
             return;
@@ -1064,6 +1072,15 @@
 
         mExecutor.execute(() -> {
             Trace.beginSection("ScreenDecorations#onConfigurationChanged");
+            mContext.getDisplay().getDisplayInfo(mDisplayInfo);
+            if (displaySizeChanged(mDisplaySize, mDisplayInfo)) {
+                // We expect the display change event to happen first, but in this case, we received
+                // onConfigurationChanged first.
+                // Return so that we handle the display change event first, and then manually
+                // trigger the config update.
+                mPendingManualConfigUpdate = true;
+                return;
+            }
             int oldRotation = mRotation;
             mPendingConfigChange = false;
             updateConfiguration();
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
new file mode 100644
index 0000000..044312b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui
+
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+interface ScreenDecorationsModule {
+    /** Start ScreenDecorations. */
+    @Binds
+    @IntoMap
+    @ClassKey(ScreenDecorations::class)
+    fun bindScreenDecorationsCoreStartable(impl: ScreenDecorations): CoreStartable
+
+    /** Listen to config changes for ScreenDecorations. */
+    @Binds
+    @IntoSet
+    fun bindScreenDecorationsConfigListener(impl: ScreenDecorations): ConfigurationListener
+}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index c3f6480..01f6971 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -37,11 +37,11 @@
 import android.view.ThreadedRenderer;
 import android.view.View;
 
-import com.android.systemui.res.R;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.systemui.dagger.GlobalRootComponent;
 import com.android.systemui.dagger.SysUIComponent;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.NotificationChannels;
 
@@ -354,19 +354,6 @@
             }
             configController.onConfigurationChanged(newConfig);
             Trace.endSection();
-            int len = mServices.length;
-            for (int i = 0; i < len; i++) {
-                if (mServices[i] != null) {
-                    if (Trace.isEnabled()) {
-                        Trace.traceBegin(
-                                Trace.TRACE_TAG_APP,
-                                mServices[i].getClass().getSimpleName()
-                                        + ".onConfigurationChanged()");
-                    }
-                    mServices[i].onConfigurationChanged(newConfig);
-                    Trace.endSection();
-                }
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
index 24aa11e..8c2d221 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.accessibility.data.repository.ColorCorrectionRepository
 import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl
+import com.android.systemui.accessibility.data.repository.ColorInversionRepository
+import com.android.systemui.accessibility.data.repository.ColorInversionRepositoryImpl
 import com.android.systemui.accessibility.qs.QSAccessibilityModule
 import dagger.Binds
 import dagger.Module
@@ -25,7 +27,8 @@
 @Module(includes = [QSAccessibilityModule::class])
 interface AccessibilityModule {
     @Binds
-    abstract fun colorCorrectionRepository(
-        impl: ColorCorrectionRepositoryImpl
-    ): ColorCorrectionRepository
+    fun colorCorrectionRepository(impl: ColorCorrectionRepositoryImpl): ColorCorrectionRepository
+
+    @Binds
+    fun colorInversionRepository(impl: ColorInversionRepositoryImpl): ColorInversionRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 3cb6314..3ca95e1 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -77,7 +77,7 @@
     @VisibleForTesting
     SparseArray<SparseArray<Float>> mUsersScales = new SparseArray();
 
-    private static class ControllerSupplier extends
+    private static class WindowMagnificationControllerSupplier extends
             DisplayIdIndexSupplier<WindowMagnificationController> {
 
         private final Context mContext;
@@ -86,7 +86,7 @@
         private final SysUiState mSysUiState;
         private final SecureSettings mSecureSettings;
 
-        ControllerSupplier(Context context, Handler handler,
+        WindowMagnificationControllerSupplier(Context context, Handler handler,
                 WindowMagnifierCallback windowMagnifierCallback,
                 DisplayManager displayManager, SysUiState sysUiState,
                 SecureSettings secureSettings) {
@@ -118,7 +118,7 @@
     }
 
     @VisibleForTesting
-    DisplayIdIndexSupplier<WindowMagnificationController> mMagnificationControllerSupplier;
+    DisplayIdIndexSupplier<WindowMagnificationController> mWindowMagnificationControllerSupplier;
 
     private static class SettingsSupplier extends
             DisplayIdIndexSupplier<MagnificationSettingsController> {
@@ -168,7 +168,7 @@
         mOverviewProxyService = overviewProxyService;
         mDisplayTracker = displayTracker;
         mA11yLogger = a11yLogger;
-        mMagnificationControllerSupplier = new ControllerSupplier(context,
+        mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context,
                 mHandler, mWindowMagnifierCallback,
                 displayManager, sysUiState, secureSettings);
         mMagnificationSettingsSupplier = new SettingsSupplier(context,
@@ -196,7 +196,8 @@
     private void updateSysUiStateFlag() {
         //TODO(b/187510533): support multi-display once SysuiState supports it.
         final WindowMagnificationController controller =
-                mMagnificationControllerSupplier.valueAt(mDisplayTracker.getDefaultDisplayId());
+                mWindowMagnificationControllerSupplier.valueAt(
+                        mDisplayTracker.getDefaultDisplayId());
         if (controller != null) {
             controller.updateSysUIStateFlag();
         } else {
@@ -212,7 +213,7 @@
             float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
             @Nullable IRemoteMagnificationAnimationCallback callback) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             windowMagnificationController.enableWindowMagnification(scale, centerX, centerY,
                     magnificationFrameOffsetRatioX, magnificationFrameOffsetRatioY, callback);
@@ -222,7 +223,7 @@
     @MainThread
     void setScaleForWindowMagnification(int displayId, float scale) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             windowMagnificationController.setScale(scale);
         }
@@ -231,7 +232,7 @@
     @MainThread
     void moveWindowMagnifier(int displayId, float offsetX, float offsetY) {
         final WindowMagnificationController windowMagnificationcontroller =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationcontroller != null) {
             windowMagnificationcontroller.moveWindowMagnifier(offsetX, offsetY);
         }
@@ -241,7 +242,7 @@
     void moveWindowMagnifierToPositionInternal(int displayId, float positionX, float positionY,
             IRemoteMagnificationAnimationCallback callback) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             windowMagnificationController.moveWindowMagnifierToPosition(positionX, positionY,
                     callback);
@@ -252,7 +253,7 @@
     void disableWindowMagnification(int displayId,
             @Nullable IRemoteMagnificationAnimationCallback callback) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             windowMagnificationController.deleteWindowMagnification(callback);
         }
@@ -417,7 +418,7 @@
     @MainThread
     private void onSetMagnifierSizeInternal(int displayId, int index) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             windowMagnificationController.changeMagnificationSize(index);
         }
@@ -426,7 +427,7 @@
     @MainThread
     private void onSetDiagonalScrollingInternal(int displayId, boolean enable) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             windowMagnificationController.setDiagonalScrolling(enable);
         }
@@ -435,7 +436,7 @@
     @MainThread
     private void onEditMagnifierSizeModeInternal(int displayId, boolean enable) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null && windowMagnificationController.isActivated()) {
             windowMagnificationController.setEditMagnifierSizeMode(enable);
         }
@@ -444,7 +445,7 @@
     @MainThread
     private void onModeSwitchInternal(int displayId, int newMode) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         final boolean isWindowMagnifierActivated = windowMagnificationController.isActivated();
         final boolean isSwitchToWindowMode = (newMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
         final boolean changed = isSwitchToWindowMode ^ isWindowMagnifierActivated;
@@ -463,7 +464,7 @@
     @MainThread
     private void onSettingsPanelVisibilityChangedInternal(int displayId, boolean shown) {
         final WindowMagnificationController windowMagnificationController =
-                mMagnificationControllerSupplier.get(displayId);
+                mWindowMagnificationControllerSupplier.get(displayId);
         if (windowMagnificationController != null) {
             boolean isWindowMagnifierActivated = windowMagnificationController.isActivated();
             if (isWindowMagnifierActivated) {
@@ -495,7 +496,7 @@
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println(TAG);
-        mMagnificationControllerSupplier.forEach(
+        mWindowMagnificationControllerSupplier.forEach(
                 magnificationController -> magnificationController.dump(pw));
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 7a8161e..da49201 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility;
 
 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
+
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
 
 import android.accessibilityservice.AccessibilityService;
@@ -57,6 +58,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.Assert;
 
@@ -71,7 +73,7 @@
  * Class to register system actions with accessibility framework.
  */
 @SysUISingleton
-public class SystemActions implements CoreStartable {
+public class SystemActions implements CoreStartable, ConfigurationController.ConfigurationListener {
     private static final String TAG = "SystemActions";
 
     /**
@@ -234,7 +236,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         final Locale locale = mContext.getResources().getConfiguration().getLocales().get(0);
         if (!locale.equals(mLocale)) {
             mLocale = locale;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActionsModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActionsModule.kt
new file mode 100644
index 0000000..4d6d784
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActionsModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+interface SystemActionsModule {
+    /** Start SystemActions. */
+    @Binds
+    @IntoMap
+    @ClassKey(SystemActions::class)
+    fun bindSystemActionsStartable(sysui: SystemActions): CoreStartable
+
+    /** Listen to config changes for SystemActions. */
+    @Binds @IntoSet fun bindSystemActionsConfigChanges(sysui: SystemActions): ConfigurationListener
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 0bd4859..dde9f48 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -1303,7 +1303,7 @@
         } else if (id == R.id.close_button) {
             setEditMagnifierSizeMode(false);
         } else {
-            animateBounceEffect();
+            animateBounceEffectIfNeeded();
         }
     }
 
@@ -1465,7 +1465,12 @@
         mBounceEffectDuration = duration;
     }
 
-    private void animateBounceEffect() {
+    private void animateBounceEffectIfNeeded() {
+        if (mMirrorView == null) {
+            // run the animation only if the mirror view is not null
+            return;
+        }
+
         final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView,
                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1),
                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1, mBounceEffectAnimationScale, 1));
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt
new file mode 100644
index 0000000..bbf10c5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.repository
+
+import android.os.UserHandle
+import android.provider.Settings.Secure
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/** Provides data related to color inversion. */
+interface ColorInversionRepository {
+    /** Observable for whether color inversion is enabled */
+    fun isEnabled(userHandle: UserHandle): Flow<Boolean>
+
+    /** Sets color inversion enabled state. */
+    suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean
+}
+
+@SysUISingleton
+class ColorInversionRepositoryImpl
+@Inject
+constructor(
+    @Background private val bgCoroutineContext: CoroutineContext,
+    private val secureSettings: SecureSettings,
+) : ColorInversionRepository {
+
+    override fun isEnabled(userHandle: UserHandle): Flow<Boolean> =
+        secureSettings
+            .observerFlow(userHandle.identifier, SETTING_NAME)
+            .onStart { emit(Unit) }
+            .map { secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED }
+            .distinctUntilChanged()
+            .flowOn(bgCoroutineContext)
+
+    override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean =
+        withContext(bgCoroutineContext) {
+            secureSettings.putIntForUser(
+                SETTING_NAME,
+                if (isEnabled) ENABLED else DISABLED,
+                userHandle.identifier
+            )
+        }
+
+    companion object {
+        private const val SETTING_NAME = Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
+        private const val DISABLED = 0
+        private const val ENABLED = 1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index df7fdb8..135ab35 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -31,6 +31,10 @@
 import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionTileDataInteractor
 import com.android.systemui.qs.tiles.impl.colorcorrection.domain.interactor.ColorCorrectionUserActionInteractor
 import com.android.systemui.qs.tiles.impl.colorcorrection.domain.model.ColorCorrectionTileModel
+import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMapper
+import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
+import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
+import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
@@ -87,8 +91,8 @@
     fun bindFontScalingTile(fontScalingTile: FontScalingTile): QSTileImpl<*>
 
     companion object {
-
         const val COLOR_CORRECTION_TILE_SPEC = "color_correction"
+        const val COLOR_INVERSION_TILE_SPEC = "inversion"
 
         @Provides
         @IntoMap
@@ -120,5 +124,36 @@
                 stateInteractor,
                 mapper,
             )
+
+        @Provides
+        @IntoMap
+        @StringKey(COLOR_INVERSION_TILE_SPEC)
+        fun provideColorInversionTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(COLOR_INVERSION_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.qs_invert_colors_icon_off,
+                        labelRes = R.string.quick_settings_inversion_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject ColorInversionTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(COLOR_INVERSION_TILE_SPEC)
+        fun provideColorInversionTileViewModel(
+            factory: QSTileViewModelFactory.Static<ColorInversionTileModel>,
+            mapper: ColorInversionTileMapper,
+            stateInteractor: ColorInversionTileDataInteractor,
+            userActionInteractor: ColorInversionUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(COLOR_INVERSION_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index fda23b7f..f2b55f4 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -24,13 +24,13 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockscreenCredential
 import com.android.keyguard.KeyguardSecurityModel
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationResultModel
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.pairwise
@@ -43,9 +43,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
@@ -60,12 +58,6 @@
 /** Defines interface for classes that can access authentication-related application state. */
 interface AuthenticationRepository {
     /**
-     * Emits the result whenever a PIN/Pattern/Password security challenge is attempted by the user
-     * in order to unlock the device.
-     */
-    val authenticationChallengeResult: SharedFlow<Boolean>
-
-    /**
      * The exact length a PIN should be for us to enable PIN length hinting.
      *
      * A PIN that's shorter or longer than this is not eligible for the UI to render hints showing
@@ -80,16 +72,6 @@
     val isPatternVisible: StateFlow<Boolean>
 
     /**
-     * The current authentication lockout (aka "throttling") state, set when the user has to wait
-     * before being able to try another authentication attempt. `null` indicates throttling isn't
-     * active.
-     */
-    val lockout: MutableStateFlow<AuthenticationLockoutModel?>
-
-    /** Whether throttling has occurred at least once since the last successful authentication. */
-    val hasLockoutOccurred: MutableStateFlow<Boolean>
-
-    /**
      * Whether the auto confirm feature is enabled for the currently-selected user.
      *
      * Note that the length of the PIN is also important to take into consideration, please see
@@ -98,6 +80,28 @@
     val isAutoConfirmFeatureEnabled: StateFlow<Boolean>
 
     /**
+     * The number of failed authentication attempts for the selected user since their last
+     * successful authentication.
+     */
+    val failedAuthenticationAttempts: StateFlow<Int>
+
+    /**
+     * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to
+     * attempt authentication again. Returns `null` if no lockout is active.
+     *
+     * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime].
+     *
+     * Also note that the value may change when the selected user is changed.
+     */
+    val lockoutEndTimestamp: Long?
+
+    /**
+     * Whether lockout has occurred at least once since the last successful authentication of any
+     * user.
+     */
+    val hasLockoutOccurred: StateFlow<Boolean>
+
+    /**
      * The currently-configured authentication method. This determines how the authentication
      * challenge needs to be completed in order to unlock an otherwise locked device.
      *
@@ -142,23 +146,6 @@
     /** Reports that the user has entered a temporary device lockout (throttling). */
     suspend fun reportLockoutStarted(durationMs: Int)
 
-    /** Returns the current number of failed authentication attempts. */
-    suspend fun getFailedAuthenticationAttemptCount(): Int
-
-    /**
-     * Returns the timestamp for when the current lockout will end, allowing the user to attempt
-     * authentication again.
-     *
-     * Note that this is in milliseconds and it matches [SystemClock.elapsedRealtime].
-     */
-    suspend fun getLockoutEndTimestamp(): Long
-
-    /**
-     * Sets the lockout timeout duration (time during which the user should not be allowed to
-     * attempt authentication).
-     */
-    suspend fun setLockoutDuration(durationMs: Int)
-
     /**
      * Checks the given [LockscreenCredential] to see if it's correct, returning an
      * [AuthenticationResultModel] representing what happened.
@@ -172,6 +159,8 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    flags: SceneContainerFlags,
+    private val clock: SystemClock,
     private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
     private val userRepository: UserRepository,
     private val lockPatternUtils: LockPatternUtils,
@@ -179,8 +168,6 @@
     mobileConnectionsRepository: MobileConnectionsRepository,
 ) : AuthenticationRepository {
 
-    override val authenticationChallengeResult = MutableSharedFlow<Boolean>()
-
     override val hintedPinLength: Int = 6
 
     override val isPatternVisible: StateFlow<Boolean> =
@@ -189,10 +176,6 @@
             getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
         )
 
-    override val lockout: MutableStateFlow<AuthenticationLockoutModel?> = MutableStateFlow(null)
-
-    override val hasLockoutOccurred: MutableStateFlow<Boolean> = MutableStateFlow(false)
-
     override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
         refreshingFlow(
             initialValue = false,
@@ -234,6 +217,31 @@
             getFreshValue = { userId -> lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) },
         )
 
+    private val _failedAuthenticationAttempts = MutableStateFlow(0)
+    override val failedAuthenticationAttempts: StateFlow<Int> =
+        _failedAuthenticationAttempts.asStateFlow()
+
+    override val lockoutEndTimestamp: Long?
+        get() =
+            lockPatternUtils.getLockoutAttemptDeadline(selectedUserId).takeIf {
+                clock.elapsedRealtime() < it
+            }
+
+    private val _hasLockoutOccurred = MutableStateFlow(false)
+    override val hasLockoutOccurred: StateFlow<Boolean> = _hasLockoutOccurred.asStateFlow()
+
+    init {
+        if (flags.isEnabled()) {
+            // Hydrate failedAuthenticationAttempts initially and whenever the selected user
+            // changes.
+            applicationScope.launch {
+                userRepository.selectedUserInfo.collect {
+                    _failedAuthenticationAttempts.value = getFailedAuthenticationAttemptCount()
+                }
+            }
+        }
+    }
+
     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
         return withContext(backgroundDispatcher) {
             blockingAuthenticationMethodInternal(selectedUserId)
@@ -248,35 +256,20 @@
         withContext(backgroundDispatcher) {
             if (isSuccessful) {
                 lockPatternUtils.reportSuccessfulPasswordAttempt(selectedUserId)
+                _hasLockoutOccurred.value = false
             } else {
                 lockPatternUtils.reportFailedPasswordAttempt(selectedUserId)
             }
-            authenticationChallengeResult.emit(isSuccessful)
+            _failedAuthenticationAttempts.value = getFailedAuthenticationAttemptCount()
         }
     }
 
     override suspend fun reportLockoutStarted(durationMs: Int) {
-        return withContext(backgroundDispatcher) {
+        lockPatternUtils.setLockoutAttemptDeadline(selectedUserId, durationMs)
+        withContext(backgroundDispatcher) {
             lockPatternUtils.reportPasswordLockout(durationMs, selectedUserId)
         }
-    }
-
-    override suspend fun getFailedAuthenticationAttemptCount(): Int {
-        return withContext(backgroundDispatcher) {
-            lockPatternUtils.getCurrentFailedPasswordAttempts(selectedUserId)
-        }
-    }
-
-    override suspend fun getLockoutEndTimestamp(): Long {
-        return withContext(backgroundDispatcher) {
-            lockPatternUtils.getLockoutAttemptDeadline(selectedUserId)
-        }
-    }
-
-    override suspend fun setLockoutDuration(durationMs: Int) {
-        withContext(backgroundDispatcher) {
-            lockPatternUtils.setLockoutAttemptDeadline(selectedUserId, durationMs)
-        }
+        _hasLockoutOccurred.value = true
     }
 
     override suspend fun checkCredential(
@@ -292,6 +285,12 @@
         }
     }
 
+    private suspend fun getFailedAuthenticationAttemptCount(): Int {
+        return withContext(backgroundDispatcher) {
+            lockPatternUtils.getCurrentFailedPasswordAttempts(selectedUserId)
+        }
+    }
+
     private val selectedUserId: Int
         get() = userRepository.getSelectedUserInfo().id
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 797154e..c85ffe6 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,36 +16,25 @@
 
 package com.android.systemui.authentication.domain.interactor
 
-import com.android.app.tracing.TraceUtils.Companion.withContext
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.async
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 
 /**
  * Hosts application business logic related to user authentication.
@@ -59,10 +48,7 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val repository: AuthenticationRepository,
-    private val userRepository: UserRepository,
-    private val clock: SystemClock,
 ) {
     /**
      * The currently-configured authentication method. This determines how the authentication
@@ -85,13 +71,6 @@
     val authenticationMethod: Flow<AuthenticationMethodModel> = repository.authenticationMethod
 
     /**
-     * The current authentication lockout (aka "throttling") state, set when the user has to wait
-     * before being able to try another authentication attempt. `null` indicates lockout isn't
-     * active.
-     */
-    val lockout: StateFlow<AuthenticationLockoutModel?> = repository.lockout
-
-    /**
      * Whether the auto confirm feature is enabled for the currently-selected user.
      *
      * Note that the length of the PIN is also important to take into consideration, please see
@@ -130,26 +109,35 @@
     /** Whether the pattern should be visible for the currently-selected user. */
     val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
 
+    private val _onAuthenticationResult = MutableSharedFlow<Boolean>()
     /**
      * Emits the outcome (successful or unsuccessful) whenever a PIN/Pattern/Password security
      * challenge is attempted by the user in order to unlock the device.
      */
-    val authenticationChallengeResult: SharedFlow<Boolean> =
-        repository.authenticationChallengeResult
+    val onAuthenticationResult: SharedFlow<Boolean> = _onAuthenticationResult.asSharedFlow()
 
     /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
     val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> = repository.isPinEnhancedPrivacyEnabled
 
-    private var lockoutCountdownJob: Job? = null
+    /**
+     * The number of failed authentication attempts for the selected user since the last successful
+     * authentication.
+     */
+    val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
 
-    init {
-        applicationScope.launch {
-            userRepository.selectedUserInfo
-                .map { it.id }
-                .distinctUntilChanged()
-                .collect { onSelectedUserChanged() }
-        }
-    }
+    /**
+     * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to
+     * attempt authentication again. Returns `null` if no lockout is active.
+     *
+     * To be notified whenever a lockout is started, the caller should subscribe to
+     * [onAuthenticationResult].
+     *
+     * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime].
+     *
+     * Also note that the value may change when the selected user is changed.
+     */
+    val lockoutEndTimestamp: Long?
+        get() = repository.lockoutEndTimestamp
 
     /**
      * Returns the currently-configured authentication method. This determines how the
@@ -190,7 +178,7 @@
         val skipCheck =
             when {
                 // Lockout is active, the UI layer should not have called this; skip the attempt.
-                lockout.value != null -> true
+                repository.lockoutEndTimestamp != null -> true
                 // The input is too short; skip the attempt.
                 input.isTooShort(authMethod) -> true
                 // Auto-confirm attempt when the feature is not enabled; skip the attempt.
@@ -211,27 +199,16 @@
         credential.zeroize()
 
         if (authenticationResult.isSuccessful || !tryAutoConfirm) {
-            repository.reportAuthenticationAttempt(
-                isSuccessful = authenticationResult.isSuccessful,
-            )
+            repository.reportAuthenticationAttempt(authenticationResult.isSuccessful)
         }
 
         // Check if lockout should start and, if so, kick off the countdown:
         if (!authenticationResult.isSuccessful && authenticationResult.lockoutDurationMs > 0) {
-            repository.apply {
-                setLockoutDuration(durationMs = authenticationResult.lockoutDurationMs)
-                reportLockoutStarted(durationMs = authenticationResult.lockoutDurationMs)
-                hasLockoutOccurred.value = true
-            }
-            startLockoutCountdown()
+            repository.reportLockoutStarted(authenticationResult.lockoutDurationMs)
         }
 
-        if (authenticationResult.isSuccessful) {
-            // Since authentication succeeded, refresh lockout to make sure the state is completely
-            // reflecting the upstream source of truth.
-            refreshLockout()
-
-            repository.hasLockoutOccurred.value = false
+        if (authenticationResult.isSuccessful || !tryAutoConfirm) {
+            _onAuthenticationResult.emit(authenticationResult.isSuccessful)
         }
 
         return if (authenticationResult.isSuccessful) {
@@ -249,54 +226,6 @@
         }
     }
 
-    /** Starts refreshing the lockout state every second. */
-    private suspend fun startLockoutCountdown() {
-        cancelLockoutCountdown()
-        lockoutCountdownJob =
-            applicationScope.launch {
-                while (refreshLockout()) {
-                    delay(1.seconds.inWholeMilliseconds)
-                }
-            }
-    }
-
-    /** Cancels any lockout state countdown started in [startLockoutCountdown]. */
-    private fun cancelLockoutCountdown() {
-        lockoutCountdownJob?.cancel()
-        lockoutCountdownJob = null
-    }
-
-    /** Notifies that the currently-selected user has changed. */
-    private suspend fun onSelectedUserChanged() {
-        cancelLockoutCountdown()
-        if (refreshLockout()) {
-            startLockoutCountdown()
-        }
-    }
-
-    /**
-     * Refreshes the lockout state, hydrating the repository with the latest state.
-     *
-     * @return Whether lockout is active or not.
-     */
-    private suspend fun refreshLockout(): Boolean {
-        withContext("$TAG#refreshLockout", backgroundDispatcher) {
-            val failedAttemptCount = async { repository.getFailedAuthenticationAttemptCount() }
-            val deadline = async { repository.getLockoutEndTimestamp() }
-            val remainingMs = max(0, deadline.await() - clock.elapsedRealtime())
-            repository.lockout.value =
-                if (remainingMs > 0) {
-                    AuthenticationLockoutModel(
-                        failedAttemptCount = failedAttemptCount.await(),
-                        remainingSeconds = ceil(remainingMs / 1000f).toInt(),
-                    )
-                } else {
-                    null // Lockout ended.
-                }
-        }
-        return repository.lockout.value != null
-    }
-
     private fun AuthenticationMethodModel.createCredential(
         input: List<Any>
     ): LockscreenCredential? {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index ab23564..83d415f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -403,6 +403,13 @@
 
             final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
                     R.layout.biometric_prompt_layout, null, false);
+            /**
+             * View is only set visible in BiometricViewSizeBinder once PromptSize is determined
+             * that accounts for iconView size, to prevent prompt resizing being visible to the
+             * user.
+             * TODO(b/288175072): May be able to remove this once constraint layout is implemented
+             */
+            view.setVisibility(View.INVISIBLE);
             mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
                     // TODO(b/201510778): This uses the wrong timeout in some cases
                     getJankListener(view, TRANSIT,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 5fba761..8a1a2da 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -84,6 +84,7 @@
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.Execution;
 
@@ -114,8 +115,12 @@
  * {@link com.android.keyguard.KeyguardUpdateMonitor}
  */
 @SysUISingleton
-public class AuthController implements CoreStartable, CommandQueue.Callbacks,
-        AuthDialogCallback, DozeReceiver {
+public class AuthController implements
+        CoreStartable,
+        ConfigurationController.ConfigurationListener,
+        CommandQueue.Callbacks,
+        AuthDialogCallback,
+        DozeReceiver {
 
     private static final String TAG = "AuthController";
     private static final boolean DEBUG = true;
@@ -1297,7 +1302,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         updateSensorLocations();
 
         // TODO(b/287311775): consider removing this to retain the UI cleanly vs re-creating
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 66fb8ca..7d9ec08 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -23,16 +23,14 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
 import com.android.systemui.Dumpable
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.util.ViewController
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import java.io.PrintWriter
 
@@ -49,7 +47,7 @@
 abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>(
     view: T,
     protected val statusBarStateController: StatusBarStateController,
-    protected val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    protected val shadeInteractor: ShadeInteractor,
     protected val dialogManager: SystemUIDialogManager,
     private val dumpManager: DumpManager
 ) : ViewController<T>(view), Dumpable {
@@ -94,20 +92,18 @@
             // can make the view not visible; and we still want to listen for events
             // that may make the view visible again.
             repeatOnLifecycle(Lifecycle.State.CREATED) {
-                listenForBouncerExpansion(this)
+                listenForShadeExpansion(this)
             }
         }
     }
 
     @VisibleForTesting
-    open suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
+    suspend fun listenForShadeExpansion(scope: CoroutineScope): Job {
         return scope.launch {
-            primaryBouncerInteractor.bouncerExpansion.map { 1f - it }.collect { expansion: Float ->
-                if (statusBarStateController.state != SHADE) {
-                    notificationShadeVisible = expansion > 0f
-                    view.onExpansionChanged(expansion)
-                    updatePauseAuth()
-                }
+            shadeInteractor.anyExpansion.collect { expansion ->
+                notificationShadeVisible = expansion > 0f
+                view.onExpansionChanged(expansion)
+                updatePauseAuth()
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
index 03749a9..e7b0d9f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
@@ -15,9 +15,9 @@
  */
 package com.android.systemui.biometrics
 
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 
 /**
@@ -26,13 +26,13 @@
 class UdfpsBpViewController(
     view: UdfpsBpView,
     statusBarStateController: StatusBarStateController,
-    primaryBouncerInteractor: PrimaryBouncerInteractor,
+    shadeInteractor: ShadeInteractor,
     systemUIDialogManager: SystemUIDialogManager,
     dumpManager: DumpManager
 ) : UdfpsAnimationViewController<UdfpsBpView>(
     view,
     statusBarStateController,
-    primaryBouncerInteractor,
+    shadeInteractor,
     systemUIDialogManager,
     dumpManager
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 65668b5..2fd13b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -86,11 +86,10 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter;
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -115,7 +114,6 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
-import javax.inject.Provider;
 
 import kotlinx.coroutines.ExperimentalCoroutinesApi;
 
@@ -152,7 +150,6 @@
     @NonNull private final SystemUIDialogManager mDialogManager;
     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
-    @NonNull private final Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels;
     @NonNull private final VibratorHelper mVibrator;
     @NonNull private final FalsingManager mFalsingManager;
     @NonNull private final PowerManager mPowerManager;
@@ -166,6 +163,7 @@
     @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener;
     @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
     @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    @NonNull private final ShadeInteractor mShadeInteractor;
     @Nullable private final TouchProcessor mTouchProcessor;
     @NonNull private final SessionTracker mSessionTracker;
     @NonNull private final Lazy<DeviceEntryUdfpsTouchOverlayViewModel>
@@ -294,7 +292,8 @@
                         mKeyguardTransitionInteractor,
                         mSelectedUserInteractor,
                         mDeviceEntryUdfpsTouchOverlayViewModel,
-                        mDefaultUdfpsTouchOverlayViewModel
+                        mDefaultUdfpsTouchOverlayViewModel,
+                        mShadeInteractor
                     )));
         }
 
@@ -623,7 +622,7 @@
         } else {
             onKeyguard = mOverlay != null
                     && mOverlay.getAnimationViewController()
-                        instanceof UdfpsKeyguardViewControllerAdapter;
+                        instanceof UdfpsKeyguardViewControllerLegacy;
         }
         return onKeyguard
                 && mKeyguardStateController.canDismissLockScreen()
@@ -660,13 +659,13 @@
             @NonNull ActivityLaunchAnimator activityLaunchAnimator,
             @NonNull @BiometricsBackground Executor biometricsExecutor,
             @NonNull PrimaryBouncerInteractor primaryBouncerInteractor,
+            @NonNull ShadeInteractor shadeInteractor,
             @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor,
             @NonNull SessionTracker sessionTracker,
             @NonNull AlternateBouncerInteractor alternateBouncerInteractor,
             @NonNull InputManager inputManager,
             @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
             @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate,
-            @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider,
             @NonNull SelectedUserInteractor selectedUserInteractor,
             @NonNull FpsUnlockTracker fpsUnlockTracker,
             @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor,
@@ -710,6 +709,7 @@
 
         mBiometricExecutor = biometricsExecutor;
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
+        mShadeInteractor = shadeInteractor;
         mAlternateBouncerInteractor = alternateBouncerInteractor;
         mInputManager = inputManager;
         mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate;
@@ -737,7 +737,6 @@
                     return Unit.INSTANCE;
                 });
         mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
-        mUdfpsKeyguardViewModels = udfpsKeyguardViewModelsProvider;
 
         final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController();
         mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index dae6d08..b94a177 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -55,9 +55,9 @@
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
@@ -108,6 +108,7 @@
     private val selectedUserInteractor: SelectedUserInteractor,
     private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
     private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
+    private val shadeInteractor: ShadeInteractor,
 ) {
     private var overlayViewLegacy: UdfpsView? = null
         private set
@@ -278,7 +279,7 @@
                         updateAccessibilityViewLocation(sensorBounds)
                     },
                     statusBarStateController,
-                    primaryBouncerInteractor,
+                    shadeInteractor,
                     dialogManager,
                     dumpManager
                 )
@@ -304,6 +305,7 @@
                     udfpsKeyguardAccessibilityDelegate,
                     selectedUserInteractor,
                     transitionInteractor,
+                    shadeInteractor,
                 )
             }
             REASON_AUTH_BP -> {
@@ -311,7 +313,7 @@
                 UdfpsBpViewController(
                     view.addUdfpsView(R.layout.udfps_bp_view),
                     statusBarStateController,
-                    primaryBouncerInteractor,
+                    shadeInteractor,
                     dialogManager,
                     dumpManager
                 )
@@ -321,7 +323,7 @@
                 UdfpsFpmEmptyViewController(
                     view.addUdfpsView(R.layout.udfps_fpm_empty_view),
                     statusBarStateController,
-                    primaryBouncerInteractor,
+                    shadeInteractor,
                     dialogManager,
                     dumpManager
                 )
@@ -438,7 +440,7 @@
             if (DeviceEntryUdfpsRefactor.isEnabled) {
                 !keyguardStateController.isShowing
             } else {
-                animation !is UdfpsKeyguardViewControllerAdapter
+                animation !is UdfpsKeyguardViewControllerLegacy
             }
 
         if (keyguardNotShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt
index 88002e7..ab3fbb1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt
@@ -15,9 +15,9 @@
  */
 package com.android.systemui.biometrics
 
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 
 /**
@@ -28,13 +28,13 @@
 class UdfpsFpmEmptyViewController(
     view: UdfpsFpmEmptyView,
     statusBarStateController: StatusBarStateController,
-    primaryBouncerInteractor: PrimaryBouncerInteractor,
+    shadeInteractor: ShadeInteractor,
     systemUIDialogManager: SystemUIDialogManager,
     dumpManager: DumpManager
 ) : UdfpsAnimationViewController<UdfpsFpmEmptyView>(
     view,
     statusBarStateController,
-    primaryBouncerInteractor,
+    shadeInteractor,
     systemUIDialogManager,
     dumpManager
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index 63fe26a..9f17024 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -33,10 +33,10 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -69,20 +69,20 @@
     systemUIDialogManager: SystemUIDialogManager,
     private val udfpsController: UdfpsController,
     private val activityLaunchAnimator: ActivityLaunchAnimator,
-    primaryBouncerInteractor: PrimaryBouncerInteractor,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
     private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
     private val selectedUserInteractor: SelectedUserInteractor,
     private val transitionInteractor: KeyguardTransitionInteractor,
+    shadeInteractor: ShadeInteractor,
 ) :
     UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>(
         view,
         statusBarStateController,
-        primaryBouncerInteractor,
+        shadeInteractor,
         systemUIDialogManager,
         dumpManager,
-    ),
-    UdfpsKeyguardViewControllerAdapter {
+    ) {
     private val uniqueIdentifier = this.toString()
     private var showingUdfpsBouncer = false
     private var udfpsRequested = false
@@ -199,11 +199,27 @@
                 listenForAodToOccludedTransitions(this)
                 listenForAlternateBouncerToAodTransitions(this)
                 listenForDreamingToAodTransitions(this)
+                listenForPrimaryBouncerToAodTransitions(this)
             }
         }
     }
 
     @VisibleForTesting
+    suspend fun listenForPrimaryBouncerToAodTransitions(scope: CoroutineScope): Job {
+        return scope.launch {
+            transitionInteractor
+                .transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD)
+                .collect { transitionStep ->
+                    view.onDozeAmountChanged(
+                        transitionStep.value,
+                        transitionStep.value,
+                        ANIMATE_APPEAR_ON_SCREEN_OFF,
+                    )
+                }
+        }
+    }
+
+    @VisibleForTesting
     suspend fun listenForDreamingToAodTransitions(scope: CoroutineScope): Job {
         return scope.launch {
             transitionInteractor.transition(KeyguardState.DREAMING, KeyguardState.AOD).collect {
@@ -305,7 +321,7 @@
     }
 
     @VisibleForTesting
-    override suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
+    suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
         return scope.launch {
             primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
                 inputBouncerExpansion = bouncerExpansion
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 8ae6f87..307b985 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -19,6 +19,7 @@
 import android.content.res.Resources
 import com.android.internal.R
 import com.android.systemui.CoreStartable
+import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.EllipseOverlapDetectorParams
 import com.android.systemui.biometrics.UdfpsUtils
 import com.android.systemui.biometrics.data.repository.BiometricStatusRepository
@@ -38,18 +39,30 @@
 import com.android.systemui.biometrics.udfps.OverlapDetector
 import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.android.systemui.util.concurrency.ThreadFactory
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
 import java.util.concurrent.Executor
 import javax.inject.Qualifier
 
 /** Dagger module for all things biometric. */
 @Module
 interface BiometricsModule {
+    /** Starts AuthController.  */
+    @Binds
+    @IntoMap
+    @ClassKey(AuthController::class)
+    fun bindAuthControllerStartable(service: AuthController): CoreStartable
+
+    /** Listen to config changes for AuthController. */
+    @Binds
+    @IntoSet
+    fun bindAuthControllerConfigChanges(service: AuthController): ConfigurationListener
 
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
index 38043b4..f4a2811 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics.domain.interactor
 
+import android.content.Context
 import android.view.MotionEvent
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -23,12 +24,15 @@
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /** Encapsulates business logic for interacting with the UDFPS overlay. */
@@ -36,10 +40,12 @@
 class UdfpsOverlayInteractor
 @Inject
 constructor(
+    @Application context: Context,
     private val authController: AuthController,
     private val selectedUserInteractor: SelectedUserInteractor,
     @Application scope: CoroutineScope
 ) {
+    private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.udfps_icon_size)
 
     /** Whether a touch is within the under-display fingerprint sensor area */
     fun isTouchWithinUdfpsArea(ev: MotionEvent): Boolean {
@@ -70,6 +76,14 @@
             }
             .stateIn(scope, started = SharingStarted.Eagerly, initialValue = UdfpsOverlayParams())
 
+    // Padding between the fingerprint icon and its bounding box in pixels.
+    val iconPadding: Flow<Int> =
+        udfpsOverlayParams.map { params ->
+            val sensorWidth = params.nativeSensorBounds.right - params.nativeSensorBounds.left
+            val nativePadding = (sensorWidth - iconSize) / 2
+            (nativePadding * params.scaleFactor).toInt()
+        }
+
     companion object {
         private const val TAG = "UdfpsOverlayInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 90e4a38..a7fb6f7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -97,7 +97,13 @@
 
         val iconOverlayView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
         val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
-
+        /**
+         * View is only set visible in BiometricViewSizeBinder once PromptSize is determined that
+         * accounts for iconView size, to prevent prompt resizing being visible to the user.
+         *
+         * TODO(b/288175072): May be able to remove this once constraint layout is implemented
+         */
+        iconView.addLottieOnCompositionLoadedListener { viewModel.setIsIconViewLoaded(true) }
         PromptIconViewBinder.bind(
             iconView,
             iconOverlayView,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 7e16d1e..f340bd8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -30,7 +30,6 @@
 import androidx.core.view.doOnLayout
 import androidx.core.view.isGone
 import androidx.lifecycle.lifecycleScope
-import com.android.systemui.res.R
 import com.android.systemui.biometrics.AuthPanelController
 import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.ui.BiometricPromptLayout
@@ -41,6 +40,8 @@
 import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
 import com.android.systemui.biometrics.ui.viewmodel.isSmall
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 /** Helper for [BiometricViewBinder] to handle resize transitions. */
@@ -92,8 +93,22 @@
             // TODO(b/251476085): migrate the legacy panel controller and simplify this
             view.repeatWhenAttached {
                 var currentSize: PromptSize? = null
+
                 lifecycleScope.launch {
-                    viewModel.size.collect { size ->
+                    /**
+                     * View is only set visible in BiometricViewSizeBinder once PromptSize is
+                     * determined that accounts for iconView size, to prevent prompt resizing being
+                     * visible to the user.
+                     *
+                     * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
+                     *   layout is implemented
+                     */
+                    combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
+                        (isIconViewLoaded, size) ->
+                        if (!isIconViewLoaded) {
+                            return@collect
+                        }
+
                         // prepare for animated size transitions
                         for (v in viewsToHideWhenSmall) {
                             v.showTextOrHide(forceHide = size.isSmall)
@@ -196,8 +211,9 @@
                                     }
                                 }
                             }
-
                             currentSize = size
+                            view.visibility = View.VISIBLE
+                            viewModel.setIsIconViewLoaded(false)
                             notifyAccessibilityChanged()
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index a8c9446..c36e0e2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -47,6 +47,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.sample
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
@@ -59,49 +60,56 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     @Application private val applicationContext: Context,
-    private val biometricStatusInteractor: BiometricStatusInteractor,
-    private val displayStateInteractor: DisplayStateInteractor,
-    private val deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor,
-    private val fpsUnlockTracker: FpsUnlockTracker,
-    private val layoutInflater: LayoutInflater,
-    private val sideFpsProgressBarViewModel: SideFpsProgressBarViewModel,
-    private val sfpsSensorInteractor: SideFpsSensorInteractor,
-    private val windowManager: WindowManager
+    private val biometricStatusInteractor: Lazy<BiometricStatusInteractor>,
+    private val displayStateInteractor: Lazy<DisplayStateInteractor>,
+    private val deviceEntrySideFpsOverlayInteractor: Lazy<DeviceEntrySideFpsOverlayInteractor>,
+    private val fpsUnlockTracker: Lazy<FpsUnlockTracker>,
+    private val layoutInflater: Lazy<LayoutInflater>,
+    private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>,
+    private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
+    private val windowManager: Lazy<WindowManager>
 ) : CoreStartable {
 
     override fun start() {
         if (!SideFpsControllerRefactor.isEnabled) {
             return
         }
+
         applicationScope
             .launch {
-                combine(
-                        biometricStatusInteractor.sfpsAuthenticationReason,
-                        deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry,
-                        sideFpsProgressBarViewModel.isVisible,
-                        ::Triple
-                    )
-                    .sample(displayStateInteractor.isInRearDisplayMode, ::Pair)
-                    .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
-                        val (
-                            systemServerAuthReason,
-                            showIndicatorForDeviceEntry,
-                            progressBarIsVisible) =
-                            combinedFlows
-                        if (!isInRearDisplayMode) {
-                            if (progressBarIsVisible) {
-                                hide()
-                            } else if (systemServerAuthReason != NotRunning) {
-                                show()
-                            } else if (showIndicatorForDeviceEntry) {
-                                show()
-                            } else {
-                                hide()
+                sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
+                    if (isSfpsAvailable) {
+                        combine(
+                                biometricStatusInteractor.get().sfpsAuthenticationReason,
+                                deviceEntrySideFpsOverlayInteractor
+                                    .get()
+                                    .showIndicatorForDeviceEntry,
+                                sideFpsProgressBarViewModel.get().isVisible,
+                                ::Triple
+                            )
+                            .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
+                            .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
+                                val (
+                                    systemServerAuthReason,
+                                    showIndicatorForDeviceEntry,
+                                    progressBarIsVisible) =
+                                    combinedFlows
+                                if (!isInRearDisplayMode) {
+                                    if (progressBarIsVisible) {
+                                        hide()
+                                    } else if (systemServerAuthReason != NotRunning) {
+                                        show()
+                                    } else if (showIndicatorForDeviceEntry) {
+                                        show()
+                                    } else {
+                                        hide()
+                                    }
+                                }
                             }
-                        }
                     }
+                }
             }
-            .invokeOnCompletion { fpsUnlockTracker.stopTracking() }
+            .invokeOnCompletion { fpsUnlockTracker.get().stopTracking() }
     }
 
     private var overlayView: View? = null
@@ -113,29 +121,29 @@
             if (it.isAttachedToWindow) {
                 lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
                 lottie?.pauseAnimation()
-                windowManager.removeView(it)
+                windowManager.get().removeView(it)
             }
         }
 
-        overlayView = layoutInflater.inflate(R.layout.sidefps_view, null, false)
+        overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false)
         val overlayViewModel =
             SideFpsOverlayViewModel(
                 applicationContext,
-                biometricStatusInteractor,
-                deviceEntrySideFpsOverlayInteractor,
-                displayStateInteractor,
-                sfpsSensorInteractor,
-                sideFpsProgressBarViewModel
+                biometricStatusInteractor.get(),
+                deviceEntrySideFpsOverlayInteractor.get(),
+                displayStateInteractor.get(),
+                sfpsSensorInteractor.get(),
+                sideFpsProgressBarViewModel.get()
             )
-        bind(overlayView!!, overlayViewModel, fpsUnlockTracker, windowManager)
+        bind(overlayView!!, overlayViewModel, fpsUnlockTracker.get(), windowManager.get())
         overlayView!!.visibility = View.INVISIBLE
-        windowManager.addView(overlayView, overlayViewModel.defaultOverlayViewParams)
+        windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
     }
 
     /** Hide the side fingerprint sensor indicator */
     private fun hide() {
         if (overlayView != null) {
-            windowManager.removeView(overlayView)
+            windowManager.get().removeView(overlayView)
             overlayView = null
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt
deleted file mode 100644
index 6f4e1a3..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt
+++ /dev/null
@@ -1,78 +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.biometrics.ui.controller
-
-import com.android.systemui.biometrics.UdfpsAnimationViewController
-import com.android.systemui.biometrics.UdfpsKeyguardView
-import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-/** Class that coordinates non-HBM animations during keyguard authentication. */
-@ExperimentalCoroutinesApi
-open class UdfpsKeyguardViewController(
-    val view: UdfpsKeyguardView,
-    statusBarStateController: StatusBarStateController,
-    primaryBouncerInteractor: PrimaryBouncerInteractor,
-    systemUIDialogManager: SystemUIDialogManager,
-    dumpManager: DumpManager,
-    private val alternateBouncerInteractor: AlternateBouncerInteractor,
-    udfpsKeyguardViewModels: UdfpsKeyguardViewModels,
-) :
-    UdfpsAnimationViewController<UdfpsKeyguardView>(
-        view,
-        statusBarStateController,
-        primaryBouncerInteractor,
-        systemUIDialogManager,
-        dumpManager,
-    ),
-    UdfpsKeyguardViewControllerAdapter {
-    private val uniqueIdentifier = this.toString()
-    override val tag: String
-        get() = TAG
-
-    init {
-        udfpsKeyguardViewModels.bindViews(view)
-    }
-
-    public override fun onViewAttached() {
-        super.onViewAttached()
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, uniqueIdentifier)
-    }
-
-    public override fun onViewDetached() {
-        super.onViewDetached()
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(false, uniqueIdentifier)
-    }
-
-    override fun shouldPauseAuth(): Boolean {
-        return !view.isVisible()
-    }
-
-    override fun listenForTouchesOutsideView(): Boolean {
-        return true
-    }
-
-    companion object {
-        private const val TAG = "UdfpsKeyguardViewController"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt
index c19ea19..7106674 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics.ui.viewmodel
 
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.hideAffordancesRequest
@@ -26,22 +27,30 @@
 
 /**
  * View model for the UdfpsTouchOverlay for when UDFPS is being requested for device entry. Handles
- * touches as long as the device entry views are visible.
+ * touches as long as the device entry view is visible (the lockscreen or the alternate bouncer
+ * view).
  */
 @ExperimentalCoroutinesApi
 class DeviceEntryUdfpsTouchOverlayViewModel
 @Inject
 constructor(
     deviceEntryIconViewModel: DeviceEntryIconViewModel,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
     systemUIDialogManager: SystemUIDialogManager,
 ) : UdfpsTouchOverlayViewModel {
-    // TODO (b/305234447): AlternateBouncer showing overrides sysuiDialogHideAffordancesRequest
-    override val shouldHandleTouches: Flow<Boolean> =
+    private val showingUdfpsAffordance: Flow<Boolean> =
         combine(
             deviceEntryIconViewModel.deviceEntryViewAlpha,
+            alternateBouncerInteractor.isVisible,
+        ) { deviceEntryViewAlpha, alternateBouncerVisible ->
+            deviceEntryViewAlpha > ALLOW_TOUCH_ALPHA_THRESHOLD || alternateBouncerVisible
+        }
+    override val shouldHandleTouches: Flow<Boolean> =
+        combine(
+            showingUdfpsAffordance,
             systemUIDialogManager.hideAffordancesRequest,
-        ) { deviceEntryViewAlpha, dialogRequestingHideAffordances ->
-            deviceEntryViewAlpha > ALLOW_TOUCH_ALPHA_THRESHOLD && !dialogRequestingHideAffordances
+        ) { showingUdfpsAffordance, dialogRequestingHideAffordances ->
+            showingUdfpsAffordance && !dialogRequestingHideAffordances
         }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 6d0a58e..d899827e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -192,6 +192,28 @@
     val iconViewModel: PromptIconViewModel =
         PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor)
 
+    private val _isIconViewLoaded = MutableStateFlow(false)
+
+    /**
+     * For prompts with an iconView, false until the prompt's iconView animation has been loaded in
+     * the view, otherwise true by default. Used for BiometricViewSizeBinder to wait for the icon
+     * asset to be loaded before determining the prompt size.
+     */
+    val isIconViewLoaded: Flow<Boolean> =
+        combine(credentialKind, _isIconViewLoaded.asStateFlow()) { credentialKind, isIconViewLoaded
+            ->
+            if (credentialKind is PromptKind.Biometric) {
+                isIconViewLoaded
+            } else {
+                true
+            }
+        }
+
+    // Sets whether the prompt's iconView animation has been loaded in the view yet.
+    fun setIsIconViewLoaded(iconViewLoaded: Boolean) {
+        _isIconViewLoaded.value = iconViewLoaded
+    }
+
     /** Padding for prompt UI elements */
     val promptPadding: Flow<Rect> =
         combine(size, displayStateInteractor.currentRotation) { size, rotation ->
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index 84f7dd5..af32eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -29,9 +29,10 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 /** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */
 @SysUISingleton
@@ -52,19 +53,13 @@
     private val alternateBouncerUiAvailableFromSource: HashSet<String> = HashSet()
     private val alternateBouncerSupported: StateFlow<Boolean> =
         if (DeviceEntryUdfpsRefactor.isEnabled) {
-            // The device entry udfps refactor doesn't currently support the alternate bouncer.
-            // TODO: Re-enable when b/287599719 is ready.
-            MutableStateFlow(false).asStateFlow()
-            //            fingerprintPropertyRepository.sensorType
-            //                .map { sensorType ->
-            //                    sensorType.isUdfps() || sensorType ==
-            // FingerprintSensorType.POWER_BUTTON
-            //                }
-            //                .stateIn(
-            //                    scope = scope,
-            //                    started = SharingStarted.Eagerly,
-            //                    initialValue = false,
-            //                )
+            fingerprintPropertyRepository.sensorType
+                .map { sensorType -> sensorType.isUdfps() || sensorType.isPowerButton() }
+                .stateIn(
+                    scope = scope,
+                    started = SharingStarted.Eagerly,
+                    initialValue = false,
+                )
         } else {
             bouncerRepository.alternateBouncerUIAvailable
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 724c0fe..1095abe 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.classifier.FalsingClassifier
@@ -29,17 +28,15 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.util.kotlin.pairwise
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
 /** Encapsulates business logic and application state accessing use-cases. */
@@ -52,33 +49,12 @@
     private val repository: BouncerRepository,
     private val authenticationInteractor: AuthenticationInteractor,
     private val keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor,
-    flags: SceneContainerFlags,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
 ) {
-
-    /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<String?> =
-        combine(repository.message, authenticationInteractor.lockout) { message, lockout ->
-                messageOrLockoutMessage(message, lockout)
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue =
-                    messageOrLockoutMessage(
-                        repository.message.value,
-                        authenticationInteractor.lockout.value,
-                    )
-            )
-
-    /**
-     * The current authentication lockout (aka "throttling") state, set when the user has to wait
-     * before being able to try another authentication attempt. `null` indicates lockout isn't
-     * active.
-     */
-    val lockout: StateFlow<AuthenticationLockoutModel?> = authenticationInteractor.lockout
+    /** The user-facing message to show in the bouncer when lockout is not active. */
+    val message: StateFlow<String?> = repository.message
 
     /** Whether the auto confirm feature is enabled for the currently-selected user. */
     val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
@@ -101,18 +77,13 @@
     /** Emits a [Unit] each time the IME (keyboard) is hidden by the user. */
     val onImeHiddenByUser: SharedFlow<Unit> = _onImeHiddenByUser
 
-    init {
-        if (flags.isEnabled()) {
-            // Clear the message if moved from locked-out to no-longer locked-out.
-            applicationScope.launch {
-                lockout.pairwise().collect { (previous, current) ->
-                    if (previous != null && current == null) {
-                        clearMessage()
-                    }
-                }
+    /** Emits a [Unit] each time a lockout is started for the selected user. */
+    val onLockoutStarted: Flow<Unit> =
+        authenticationInteractor.onAuthenticationResult
+            .filter { successfullyAuthenticated ->
+                !successfullyAuthenticated && authenticationInteractor.lockoutEndTimestamp != null
             }
-        }
-    }
+            .map {}
 
     /** Notifies that the user has places down a pointer, not necessarily dragging just yet. */
     fun onDown() {
@@ -188,7 +159,7 @@
         }
 
         if (authenticationInteractor.getAuthenticationMethod() == AuthenticationMethodModel.Sim) {
-            // We authenticate sim in SimInteractor
+            // SIM is authenticated in SimBouncerInteractor.
             return AuthenticationResult.SKIPPED
         }
 
@@ -196,18 +167,20 @@
         // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
         // This allows the task to continue running properly even when the calling scope has been
         // cancelled.
-        return applicationScope
-            .async {
-                val authResult = authenticationInteractor.authenticate(input, tryAutoConfirm)
-                if (
-                    authResult == AuthenticationResult.FAILED ||
-                        (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
-                ) {
-                    showErrorMessage()
-                }
-                authResult
-            }
-            .await()
+        val authResult =
+            applicationScope
+                .async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
+                .await()
+
+        if (authenticationInteractor.lockoutEndTimestamp != null) {
+            clearMessage()
+        } else if (
+            authResult == AuthenticationResult.FAILED ||
+                (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
+        ) {
+            showErrorMessage()
+        }
+        return authResult
     }
 
     /**
@@ -250,19 +223,4 @@
             else -> ""
         }
     }
-
-    private fun messageOrLockoutMessage(
-        message: String?,
-        lockoutModel: AuthenticationLockoutModel?,
-    ): String {
-        return when {
-            lockoutModel != null ->
-                applicationContext.getString(
-                    com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
-                    lockoutModel.remainingSeconds,
-                )
-            message != null -> message
-            else -> ""
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 4b14343..be6cf85 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.Bitmap
 import androidx.core.graphics.drawable.toBitmap
+import com.android.internal.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
@@ -34,19 +35,24 @@
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
+import com.android.systemui.util.time.SystemClock
 import dagger.Module
 import dagger.Provides
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.job
@@ -58,13 +64,14 @@
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val bouncerInteractor: BouncerInteractor,
-    authenticationInteractor: AuthenticationInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
     flags: SceneContainerFlags,
     selectedUser: Flow<UserViewModel>,
     users: Flow<List<UserViewModel>>,
     userSwitcherMenu: Flow<List<UserActionViewModel>>,
     actionButtonInteractor: BouncerActionButtonInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
+    private val clock: SystemClock,
 ) {
     val selectedUserImage: StateFlow<Bitmap?> =
         selectedUser
@@ -104,15 +111,6 @@
     val isUserSwitcherVisible: Boolean
         get() = bouncerInteractor.isUserSwitcherVisible
 
-    private val isInputEnabled: StateFlow<Boolean> =
-        bouncerInteractor.lockout
-            .map { it == null }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = bouncerInteractor.lockout.value == null,
-            )
-
     // Handle to the scope of the child ViewModel (stored in [authMethod]).
     private var childViewModelScope: CoroutineScope? = null
     private val _dialogMessage = MutableStateFlow<String?>(null)
@@ -138,19 +136,22 @@
      */
     val dialogMessage: StateFlow<String?> = _dialogMessage.asStateFlow()
 
+    /**
+     * A message shown when the user has attempted the wrong credential too many times and now must
+     * wait a while before attempting to authenticate again.
+     *
+     * This is updated every second (countdown) during the lockout duration. When lockout is not
+     * active, this is `null` and no lockout message should be shown.
+     */
+    private val lockoutMessage = MutableStateFlow<String?>(null)
+
     /** The user-facing message to show in the bouncer. */
     val message: StateFlow<MessageViewModel> =
-        combine(bouncerInteractor.message, bouncerInteractor.lockout) { message, lockout ->
-                toMessageViewModel(message, isLockedOut = lockout != null)
-            }
+        combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue =
-                    toMessageViewModel(
-                        message = bouncerInteractor.message.value,
-                        isLockedOut = bouncerInteractor.lockout.value != null,
-                    ),
+                initialValue = createMessageViewModel(),
             )
 
     /**
@@ -194,24 +195,29 @@
                 initialValue = isFoldSplitRequired(authMethodViewModel.value),
             )
 
+    private val isInputEnabled: StateFlow<Boolean> =
+        lockoutMessage
+            .map { it == null }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = authenticationInteractor.lockoutEndTimestamp == null,
+            )
+
+    private var lockoutCountdownJob: Job? = null
+
     init {
         if (flags.isEnabled()) {
             applicationScope.launch {
-                combine(bouncerInteractor.lockout, authMethodViewModel) {
-                        lockout,
-                        authMethodViewModel ->
-                        if (lockout != null && authMethodViewModel != null) {
-                            applicationContext.getString(
-                                authMethodViewModel.lockoutMessageId,
-                                lockout.failedAttemptCount,
-                                lockout.remainingSeconds,
-                            )
-                        } else {
-                            null
-                        }
-                    }
-                    .distinctUntilChanged()
-                    .collect { dialogMessage -> _dialogMessage.value = dialogMessage }
+                bouncerInteractor.onLockoutStarted.collect {
+                    showLockoutDialog()
+                    startLockoutCountdown()
+                }
+            }
+
+            applicationScope.launch {
+                // Update the lockout countdown whenever the selected user is switched.
+                selectedUser.collect { startLockoutCountdown() }
             }
         }
     }
@@ -221,6 +227,48 @@
         _dialogMessage.value = null
     }
 
+    private fun showLockoutDialog() {
+        applicationScope.launch {
+            val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
+            _dialogMessage.value =
+                authMethodViewModel.value?.lockoutMessageId?.let { messageId ->
+                    applicationContext.getString(
+                        messageId,
+                        failedAttempts,
+                        remainingLockoutSeconds()
+                    )
+                }
+        }
+    }
+
+    /** Shows the countdown message and refreshes it every second. */
+    private fun startLockoutCountdown() {
+        lockoutCountdownJob?.cancel()
+        lockoutCountdownJob =
+            applicationScope.launch {
+                do {
+                    val remainingSeconds = remainingLockoutSeconds()
+                    lockoutMessage.value =
+                        if (remainingSeconds > 0) {
+                            applicationContext.getString(
+                                R.string.lockscreen_too_many_failed_attempts_countdown,
+                                remainingSeconds,
+                            )
+                        } else {
+                            null
+                        }
+                    delay(1.seconds)
+                } while (remainingSeconds > 0)
+                lockoutCountdownJob = null
+            }
+    }
+
+    private fun remainingLockoutSeconds(): Int {
+        val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+        val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
+        return ceil(remainingMs / 1000f).toInt()
+    }
+
     private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
         return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
     }
@@ -229,12 +277,11 @@
         return authMethod !is PasswordBouncerViewModel
     }
 
-    private fun toMessageViewModel(
-        message: String?,
-        isLockedOut: Boolean,
-    ): MessageViewModel {
+    private fun createMessageViewModel(): MessageViewModel {
+        val isLockedOut = lockoutMessage.value != null
         return MessageViewModel(
-            text = message ?: "",
+            // A lockout message takes precedence over the non-lockout message.
+            text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "",
             isUpdateAnimated = !isLockedOut,
         )
     }
@@ -328,6 +375,7 @@
         userSwitcherViewModel: UserSwitcherViewModel,
         actionButtonInteractor: BouncerActionButtonInteractor,
         simBouncerInteractor: SimBouncerInteractor,
+        clock: SystemClock,
     ): BouncerViewModel {
         return BouncerViewModel(
             applicationContext = applicationContext,
@@ -341,6 +389,7 @@
             userSwitcherMenu = userSwitcherViewModel.menu,
             actionButtonInteractor = actionButtonInteractor,
             simBouncerInteractor = simBouncerInteractor,
+            clock = clock,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index b682717..8e14778 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -56,13 +56,11 @@
 
     /** Whether the UI should request focus on the text field element. */
     val isTextFieldFocusRequested =
-        combine(interactor.lockout, isTextFieldFocused) { throttling, hasFocus ->
-                throttling == null && !hasFocus
-            }
+        combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> hasInput && !hasFocus }
             .stateIn(
                 scope = viewModelScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = interactor.lockout.value == null && !isTextFieldFocused.value,
+                initialValue = isInputEnabled.value && !isTextFieldFocused.value,
             )
 
     override fun onHidden() {
@@ -104,7 +102,7 @@
      * hidden.
      */
     suspend fun onImeVisibilityChanged(isVisible: Boolean) {
-        if (isImeVisible && !isVisible && interactor.lockout.value == null) {
+        if (isImeVisible && !isVisible && isInputEnabled.value) {
             interactor.onImeHiddenByUser()
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
deleted file mode 100644
index 7be2eaf..0000000
--- a/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
+++ /dev/null
@@ -1,27 +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.common.domain
-
-import com.android.systemui.common.domain.interactor.ConfigurationInteractor
-import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
-import dagger.Binds
-import dagger.Module
-
-@Module
-abstract class CommonDomainLayerModule {
-    @Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt
deleted file mode 100644
index 89053d1..0000000
--- a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt
+++ /dev/null
@@ -1,57 +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.common.domain.interactor
-
-import android.content.res.Configuration
-import android.graphics.Rect
-import android.view.Surface
-import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-
-interface ConfigurationInteractor {
-    /**
-     * Returns screen size adjusted to rotation, so returned screen sizes are stable across all
-     * rotations, could be useful if you need to react to screen resize (e.g. fold/unfold on
-     * foldable devices)
-     */
-    val naturalMaxBounds: Flow<Rect>
-}
-
-class ConfigurationInteractorImpl
-@Inject
-constructor(private val repository: ConfigurationRepository) : ConfigurationInteractor {
-
-    override val naturalMaxBounds: Flow<Rect>
-        get() = repository.configurationValues.map { it.naturalScreenBounds }.distinctUntilChanged()
-
-    /**
-     * Returns screen size adjusted to rotation, so returned screen size is stable across all
-     * rotations
-     */
-    private val Configuration.naturalScreenBounds: Rect
-        get() {
-            val rotation = windowConfiguration.displayRotation
-            val maxBounds = windowConfiguration.maxBounds
-            return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
-                Rect(0, 0, maxBounds.width(), maxBounds.height())
-            } else {
-                Rect(0, 0, maxBounds.height(), maxBounds.width())
-            }
-        }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
index 3648f3b..1353985 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
@@ -17,17 +17,45 @@
 
 package com.android.systemui.common.ui.domain.interactor
 
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.view.Surface
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onStart
 
 /** Business logic related to configuration changes. */
 @SysUISingleton
 class ConfigurationInteractor @Inject constructor(private val repository: ConfigurationRepository) {
+    /**
+     * Returns screen size adjusted to rotation, so returned screen size is stable across all
+     * rotations
+     */
+    private val Configuration.naturalScreenBounds: Rect
+        get() {
+            val rotation = windowConfiguration.displayRotation
+            val maxBounds = windowConfiguration.maxBounds
+            return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+                Rect(0, 0, maxBounds.width(), maxBounds.height())
+            } else {
+                Rect(0, 0, maxBounds.height(), maxBounds.width())
+            }
+        }
+
+    /**
+     * Returns screen size adjusted to rotation, so returned screen sizes are stable across all
+     * rotations, could be useful if you need to react to screen resize (e.g. fold/unfold on
+     * foldable devices)
+     */
+    val naturalMaxBounds: Flow<Rect> =
+        repository.configurationValues.map { it.naturalScreenBounds }.distinctUntilChanged()
+
     /** Given [resourceId], emit the dimension pixel size on config change */
     fun dimensionPixelSize(resourceId: Int): Flow<Int> {
         return onAnyConfigurationChange.mapLatest { repository.getDimensionPixelSize(resourceId) }
@@ -43,4 +71,7 @@
     /** Emit an event on any config change */
     val onAnyConfigurationChange: Flow<Unit> =
         repository.onAnyConfigurationChange.onStart { emit(Unit) }
+
+    /** Emits the current resolution scaling factor */
+    val scaleForResolution: Flow<Float> = repository.scaleForResolution
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index a12db6f..779446d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -117,10 +117,10 @@
     fun updateItemRank(itemUid: Long, order: Int)
 
     @Transaction
-    fun updateWidgetOrder(ids: List<Int>) {
-        ids.forEachIndexed { index, it ->
-            val widget = getWidgetByIdNow(it)
-            updateItemRank(widget.itemId, ids.size - index)
+    fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {
+        widgetIdToPriorityMap.forEach { (id, priority) ->
+            val widget = getWidgetByIdNow(id)
+            updateItemRank(widget.itemId, priority)
         }
     }
 
@@ -129,7 +129,7 @@
         return insertWidget(
             widgetId = widgetId,
             componentName = provider.flattenToString(),
-            insertItemRank(priority),
+            itemId = insertItemRank(priority),
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index ded5581..d1bbe57 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -62,8 +62,12 @@
     /** Delete a widget by id from app widget service and the database. */
     fun deleteWidget(widgetId: Int) {}
 
-    /** Update the order of widgets in the database. */
-    fun updateWidgetOrder(ids: List<Int>) {}
+    /**
+     * Update the order of widgets in the database.
+     *
+     * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget.
+     */
+    fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {}
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -168,11 +172,11 @@
         }
     }
 
-    override fun updateWidgetOrder(ids: List<Int>) {
+    override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {
         applicationScope.launch(bgDispatcher) {
-            communalWidgetDao.updateWidgetOrder(ids)
+            communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap)
             logger.i({ "Updated the order of widget list with ids: $str1." }) {
-                str1 = ids.toString()
+                str1 = widgetIdToPriorityMap.toString()
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index e342c6b..0f4e583 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -102,8 +102,13 @@
     /** Delete a widget by id. */
     fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
 
-    /** Reorder widgets. The order in the list will be their display order in the hub. */
-    fun updateWidgetOrder(ids: List<Int>) = widgetRepository.updateWidgetOrder(ids)
+    /**
+     * Reorder the widgets.
+     *
+     * @param widgetIdToPriorityMap mapping of the widget ids to their new priorities.
+     */
+    fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) =
+        widgetRepository.updateWidgetOrder(widgetIdToPriorityMap)
 
     /** A list of widget content to be displayed in the communal hub. */
     val widgetContent: Flow<List<CommunalContentModel.Widget>> =
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index bb9b4b5..3ae5229 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -20,6 +20,7 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.widget.RemoteViews
 import com.android.systemui.communal.shared.model.CommunalContentSize
+import java.util.UUID
 
 /** Encapsulates data for a communal content. */
 sealed interface CommunalContentModel {
@@ -39,6 +40,13 @@
         override val size = CommunalContentSize.HALF
     }
 
+    /** A placeholder item representing a new widget being added */
+    class WidgetPlaceholder : CommunalContentModel {
+        override val key: String = "widget_placeholder_${UUID.randomUUID()}"
+        // Same as widget size.
+        override val size = CommunalContentSize.HALF
+    }
+
     class Tutorial(
         id: Int,
         override val size: CommunalContentSize,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
index c5610c87..75f9d80 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt
@@ -30,9 +30,9 @@
 import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_GLANCEABLE_HUB
 import com.android.systemui.smartspace.SmartspacePrecondition
 import com.android.systemui.smartspace.SmartspaceTargetFilter
-import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION
-import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER
 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.LOCKSCREEN_SMARTSPACE_PRECONDITION
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.LOCKSCREEN_SMARTSPACE_TARGET_FILTER
 import com.android.systemui.util.concurrency.Execution
 import java.util.Optional
 import java.util.concurrent.Executor
@@ -48,8 +48,8 @@
     private val smartspaceManager: SmartspaceManager?,
     private val execution: Execution,
     @Main private val uiExecutor: Executor,
-    @Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
-    @Named(DREAM_SMARTSPACE_TARGET_FILTER)
+    @Named(LOCKSCREEN_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
+    @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER)
     private val optionalTargetFilter: Optional<SmartspaceTargetFilter>,
     @Named(GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>,
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 708f137..577e404 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.ui.viewmodel
 
+import android.content.ComponentName
 import android.os.PowerManager
 import android.os.SystemClock
 import android.view.MotionEvent
@@ -53,6 +54,13 @@
         communalInteractor.setTransitionState(transitionState)
     }
 
+    /**
+     * Called when a widget is added via drag and drop from the widget picker into the communal hub.
+     */
+    fun onAddWidget(componentName: ComponentName, priority: Int) {
+        communalInteractor.addWidget(componentName, priority)
+    }
+
     // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block
     //  touches anymore.
     /** Called when a touch is received outside the edge swipe area when hub mode is closed. */
@@ -82,8 +90,14 @@
     /** Called as the UI requests deleting a widget. */
     open fun onDeleteWidget(id: Int) {}
 
-    /** Called as the UI requests reordering widgets. */
-    open fun onReorderWidgets(ids: List<Int>) {}
+    /**
+     * Called as the UI requests reordering widgets.
+     *
+     * @param widgetIdToPriorityMap mapping of the widget ids to its priority. When re-ordering to
+     *   add a new item in the middle, provide the priorities of existing widgets as if the new item
+     *   existed, and then, call [onAddWidget] to add the new item at intended order.
+     */
+    open fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) {}
 
     /** Called as the UI requests opening the widget editor. */
     open fun onOpenWidgetEditor() {}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index c82e000..368df9e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -47,5 +47,6 @@
 
     override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
 
-    override fun onReorderWidgets(ids: List<Int>) = communalInteractor.updateWidgetOrder(ids)
+    override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) =
+        communalInteractor.updateWidgetOrder(widgetIdToPriorityMap)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 573a748..c936c63 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -16,10 +16,13 @@
 
 package com.android.systemui.communal.widgets
 
-import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.Bundle
+import android.os.RemoteException
 import android.util.Log
+import android.view.IWindowManager
 import androidx.activity.ComponentActivity
 import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
@@ -34,12 +37,12 @@
 constructor(
     private val communalViewModel: CommunalEditModeViewModel,
     private val communalInteractor: CommunalInteractor,
+    private var windowManagerService: IWindowManager? = null,
 ) : ComponentActivity() {
     companion object {
-        /**
-         * Intent extra name for the {@link AppWidgetProviderInfo} of a widget to add to hub mode.
-         */
-        const val ADD_WIDGET_INFO = "add_widget_info"
+        private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
+        private const val EXTRA_FILTER_STRATEGY = "filter_strategy"
+        private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1
         private const val TAG = "EditWidgetsActivity"
     }
 
@@ -47,15 +50,23 @@
         registerForActivityResult(StartActivityForResult()) { result ->
             when (result.resultCode) {
                 RESULT_OK -> {
-                    result.data
-                        ?.let {
-                            it.getParcelableExtra(
-                                ADD_WIDGET_INFO,
-                                AppWidgetProviderInfo::class.java
-                            )
+                    result.data?.let { intent ->
+                        val isPendingWidgetDrag =
+                            intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
+                        // Nothing to do when a widget is being dragged & dropped. The drop
+                        // target in the communal grid will receive the widget to be added (if
+                        // the user drops it over).
+                        if (!isPendingWidgetDrag) {
+                            intent
+                                .getParcelableExtra(
+                                    Intent.EXTRA_COMPONENT_NAME,
+                                    ComponentName::class.java
+                                )
+                                ?.let { communalInteractor.addWidget(it, 0) }
+                                ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
                         }
-                        ?.let { communalInteractor.addWidget(it.provider, 0) }
-                        ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
+                    }
+                        ?: run { Log.w(TAG, "No data in result.") }
                 }
                 else ->
                     Log.w(
@@ -72,13 +83,37 @@
             activity = this,
             viewModel = communalViewModel,
             onOpenWidgetPicker = {
-                addWidgetActivityLauncher.launch(
-                    Intent(applicationContext, WidgetPickerActivity::class.java)
-                )
+                val localPackageManager: PackageManager = getPackageManager()
+                val intent =
+                    Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
+                localPackageManager
+                    .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
+                    ?.activityInfo
+                    ?.packageName
+                    ?.let { packageName ->
+                        try {
+                            addWidgetActivityLauncher.launch(
+                                Intent(Intent.ACTION_PICK).also {
+                                    it.setPackage(packageName)
+                                    it.putExtra(
+                                        EXTRA_FILTER_STRATEGY,
+                                        FILTER_STRATEGY_GLANCEABLE_HUB
+                                    )
+                                }
+                            )
+                        } catch (e: Exception) {
+                            Log.e(TAG, "Failed to launch widget picker activity", e)
+                        }
+                    }
+                    ?: run { Log.e(TAG, "Couldn't resolve launcher package name") }
             },
             onEditDone = {
-                // TODO(b/315154364): in a separate change, lock the device and transition to GH
-                finish()
+                try {
+                    checkNotNull(windowManagerService).lockNow(/* options */ null)
+                    finish()
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Couldn't lock the device as WindowManager is dead.")
+                }
             }
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
deleted file mode 100644
index a276548..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
+++ /dev/null
@@ -1,86 +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.communal.widgets
-
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProviderInfo
-import android.content.Intent
-import android.os.Bundle
-import android.util.DisplayMetrics
-import android.util.Log
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.LinearLayout
-import androidx.activity.ComponentActivity
-import com.android.systemui.res.R
-import javax.inject.Inject
-
-/**
- * An Activity responsible for displaying a list of widgets to add to the hub mode grid. This is
- * essentially a placeholder until Launcher's widget picker can be used.
- */
-class WidgetPickerActivity
-@Inject
-constructor(
-    private val appWidgetManager: AppWidgetManager,
-) : ComponentActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        setContentView(R.layout.widget_picker)
-
-        loadWidgets()
-    }
-
-    private fun loadWidgets() {
-        val containerView: ViewGroup? = findViewById(R.id.widgets_container)
-        containerView?.apply {
-            try {
-                appWidgetManager
-                    .getInstalledProviders(AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
-                    ?.stream()
-                    ?.limit(5)
-                    ?.forEach { widgetInfo ->
-                        val activity = this@WidgetPickerActivity
-                        val widgetPreview =
-                            widgetInfo.loadPreviewImage(activity, DisplayMetrics.DENSITY_HIGH)
-                        val widgetView = ImageView(activity)
-                        val lp = LinearLayout.LayoutParams(WIDGET_PREVIEW_SIZE, WIDGET_PREVIEW_SIZE)
-                        widgetView.setLayoutParams(lp)
-                        widgetView.setImageDrawable(widgetPreview)
-                        widgetView.setOnClickListener({
-                            setResult(
-                                RESULT_OK,
-                                Intent().putExtra(EditWidgetsActivity.ADD_WIDGET_INFO, widgetInfo)
-                            )
-                            finish()
-                        })
-
-                        addView(widgetView)
-                    }
-            } catch (e: RuntimeException) {
-                Log.e(TAG, "Exception fetching widget providers", e)
-            }
-        }
-    }
-
-    companion object {
-        private const val WIDGET_PREVIEW_SIZE = 400
-        private const val TAG = "WidgetPickerActivity"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 4b27af1..9afd5ed 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -20,7 +20,6 @@
 
 import com.android.systemui.ForegroundServicesDialog;
 import com.android.systemui.communal.widgets.EditWidgetsActivity;
-import com.android.systemui.communal.widgets.WidgetPickerActivity;
 import com.android.systemui.contrast.ContrastDialogActivity;
 import com.android.systemui.keyguard.WorkLockActivity;
 import com.android.systemui.people.PeopleSpaceActivity;
@@ -158,12 +157,6 @@
     @ClassKey(EditWidgetsActivity.class)
     public abstract Activity bindEditWidgetsActivity(EditWidgetsActivity activity);
 
-    /** Inject into WidgetPickerActivity. */
-    @Binds
-    @IntoMap
-    @ClassKey(WidgetPickerActivity.class)
-    public abstract Activity bindWidgetPickerActivity(WidgetPickerActivity activity);
-
     /** Inject into SwitchToManagedProfileForCallActivity. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 236c5b8..50f861f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -23,6 +23,8 @@
 import android.hardware.SensorPrivacyManager;
 
 import com.android.keyguard.KeyguardViewController;
+import com.android.systemui.ScreenDecorationsModule;
+import com.android.systemui.accessibility.SystemActionsModule;
 import com.android.systemui.battery.BatterySaverModule;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
@@ -34,6 +36,7 @@
 import com.android.systemui.power.dagger.PowerModule;
 import com.android.systemui.qs.dagger.QSModule;
 import com.android.systemui.qs.tileimpl.QSFactoryImpl;
+import com.android.systemui.reardisplay.RearDisplayModule;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsImplementation;
 import com.android.systemui.rotationlock.RotationLockModule;
@@ -59,6 +62,7 @@
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerImpl;
 import com.android.systemui.statusbar.policy.SensorPrivacyController;
 import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
+import com.android.systemui.toast.ToastModule;
 import com.android.systemui.volume.dagger.VolumeModule;
 import com.android.systemui.wallpapers.dagger.WallpaperModule;
 
@@ -89,19 +93,23 @@
         CollapsedStatusBarFragmentStartableModule.class,
         GestureModule.class,
         HeadsUpModule.class,
+        KeyboardShortcutsModule.class,
         MediaModule.class,
         MultiUserUtilsModule.class,
         NavigationBarControllerModule.class,
         PowerModule.class,
         QSModule.class,
-        ShadeModule.class,
+        RearDisplayModule.class,
         ReferenceScreenshotModule.class,
         RotationLockModule.class,
-        SceneContainerFrameworkModule.class,
+        ScreenDecorationsModule.class,
+        SystemActionsModule.class,
+        ShadeModule.class,
         StartCentralSurfacesModule.class,
+        SceneContainerFrameworkModule.class,
+        ToastModule.class,
         VolumeModule.class,
-        WallpaperModule.class,
-        KeyboardShortcutsModule.class
+        WallpaperModule.class
 })
 public abstract class ReferenceSystemUIModule {
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index d041acb..ac71664 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -19,12 +19,9 @@
 import com.android.keyguard.KeyguardBiometricLockoutLogger
 import com.android.systemui.CoreStartable
 import com.android.systemui.LatencyTester
-import com.android.systemui.ScreenDecorations
 import com.android.systemui.SliceBroadcastRelayHandler
-import com.android.systemui.accessibility.SystemActions
 import com.android.systemui.accessibility.Magnification
 import com.android.systemui.back.domain.interactor.BackActionInteractor
-import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
 import com.android.systemui.controls.dagger.StartControlsStartableModule
@@ -46,10 +43,6 @@
 import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator
 import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable
-import com.android.systemui.power.PowerUI
-import com.android.systemui.reardisplay.RearDisplayDialogController
-import com.android.systemui.recents.Recents
-import com.android.systemui.recents.ScreenPinningRequest
 import com.android.systemui.settings.dagger.MultiUserUtilsModule
 import com.android.systemui.shortcut.ShortcutKeyDispatcher
 import com.android.systemui.statusbar.ImmersiveModeConfirmation
@@ -61,11 +54,9 @@
 import com.android.systemui.stylus.StylusUsiPowerStartable
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.theme.ThemeOverlayController
-import com.android.systemui.toast.ToastUI
 import com.android.systemui.usb.StorageNotification
 import com.android.systemui.util.NotificationChannels
 import com.android.systemui.util.StartBinderLoggerModule
-import com.android.systemui.volume.VolumeUI
 import com.android.systemui.wallpapers.dagger.WallpaperModule
 import com.android.systemui.wmshell.WMShell
 import dagger.Binds
@@ -74,7 +65,12 @@
 import dagger.multibindings.IntoMap
 
 /**
- * Collection of {@link CoreStartable}s that should be run on AOSP.
+ * DEPRECATED: DO NOT ADD THINGS TO THIS FILE.
+ *
+ * Add a feature specific daggger module for what you are working on. Bind your CoreStartable there.
+ * Include that module where it is needed.
+ *
+ * @deprecated
  */
 @Module(
     includes = [
@@ -85,12 +81,6 @@
     ]
 )
 abstract class SystemUICoreStartableModule {
-    /** Inject into AuthController.  */
-    @Binds
-    @IntoMap
-    @ClassKey(AuthController::class)
-    abstract fun bindAuthController(service: AuthController): CoreStartable
-
     /** Inject into BiometricNotificationService */
     @Binds
     @IntoMap
@@ -158,18 +148,6 @@
     @PerUser
     abstract fun bindNotificationChannels(sysui: NotificationChannels): CoreStartable
 
-    /** Inject into PowerUI.  */
-    @Binds
-    @IntoMap
-    @ClassKey(PowerUI::class)
-    abstract fun bindPowerUI(sysui: PowerUI): CoreStartable
-
-    /** Inject into Recents.  */
-    @Binds
-    @IntoMap
-    @ClassKey(Recents::class)
-    abstract fun bindRecents(sysui: Recents): CoreStartable
-
     /** Inject into ImmersiveModeConfirmation.  */
     @Binds
     @IntoMap
@@ -182,12 +160,6 @@
     @ClassKey(RingtonePlayer::class)
     abstract fun bind(sysui: RingtonePlayer): CoreStartable
 
-    /** Inject into ScreenDecorations.  */
-    @Binds
-    @IntoMap
-    @ClassKey(ScreenDecorations::class)
-    abstract fun bindScreenDecorations(sysui: ScreenDecorations): CoreStartable
-
     /** Inject into GesturePointerEventHandler. */
     @Binds
     @IntoMap
@@ -218,23 +190,12 @@
     @ClassKey(StorageNotification::class)
     abstract fun bindStorageNotification(sysui: StorageNotification): CoreStartable
 
-    /** Inject into SystemActions.  */
-    @Binds
-    @IntoMap
-    @ClassKey(SystemActions::class)
-    abstract fun bindSystemActions(sysui: SystemActions): CoreStartable
-
     /** Inject into ThemeOverlayController.  */
     @Binds
     @IntoMap
     @ClassKey(ThemeOverlayController::class)
     abstract fun bindThemeOverlayController(sysui: ThemeOverlayController): CoreStartable
 
-    /** Inject into ToastUI.  */
-    @Binds
-    @IntoMap
-    @ClassKey(ToastUI::class)
-    abstract fun bindToastUI(service: ToastUI): CoreStartable
 
     /** Inject into MediaOutputSwitcherDialogUI.  */
     @Binds
@@ -242,12 +203,6 @@
     @ClassKey(MediaOutputSwitcherDialogUI::class)
     abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable
 
-    /** Inject into VolumeUI.  */
-    @Binds
-    @IntoMap
-    @ClassKey(VolumeUI::class)
-    abstract fun bindVolumeUI(sysui: VolumeUI): CoreStartable
-
     /** Inject into Magnification.  */
     @Binds
     @IntoMap
@@ -293,11 +248,6 @@
     abstract fun bindChipbarController(sysui: ChipbarCoordinator): CoreStartable
 
 
-    /** Inject into RearDisplayDialogController) */
-    @Binds
-    @IntoMap
-    @ClassKey(RearDisplayDialogController::class)
-    abstract fun bindRearDisplayDialogController(sysui: RearDisplayDialogController): CoreStartable
 
     /** Inject into StylusUsiPowerStartable) */
     @Binds
@@ -361,9 +311,4 @@
     @IntoMap
     @ClassKey(KeyguardDismissBinder::class)
     abstract fun bindKeyguardDismissBinder(impl: KeyguardDismissBinder): CoreStartable
-
-    @Binds
-    @IntoMap
-    @ClassKey(ScreenPinningRequest::class)
-    abstract fun bindScreenPinningRequest(impl: ScreenPinningRequest): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index ca8268d..a25c788 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -43,7 +43,6 @@
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
 import com.android.systemui.common.data.CommonDataLayerModule;
-import com.android.systemui.common.domain.CommonDomainLayerModule;
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
 import com.android.systemui.controls.dagger.ControlsModule;
@@ -179,7 +178,6 @@
         ClockRegistryModule.class,
         CommunalModule.class,
         CommonDataLayerModule.class,
-        CommonDomainLayerModule.class,
         ConnectivityModule.class,
         ControlsModule.class,
         CoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 47be8ab..f6a9570 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -159,7 +159,7 @@
     fun attemptDeviceEntry() {
         // TODO (b/307768356),
         //       1. Check if the device is already authenticated by trust agent/passive biometrics
-        //       2. show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show
+        //       2. Show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show
         //       3. For face auth only setups trigger face auth, delay transitioning to bouncer for
         //          a small amount of time.
         //       4. Transition to bouncer scene
@@ -197,8 +197,8 @@
     init {
         if (flags.isEnabled()) {
             applicationScope.launch {
-                authenticationInteractor.authenticationChallengeResult.collectLatest { successful ->
-                    if (successful) {
+                authenticationInteractor.onAuthenticationResult.collectLatest { isSuccessful ->
+                    if (isSuccessful) {
                         repository.reportSuccessfulAuthentication()
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
new file mode 100644
index 0000000..ef2537c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.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.deviceentry.ui.binder
+
+import android.annotation.SuppressLint
+import androidx.core.view.isInvisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
+import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+object UdfpsAccessibilityOverlayBinder {
+
+    /** Forwards hover events to the view model to make guided announcements for accessibility. */
+    @SuppressLint("ClickableViewAccessibility")
+    @JvmStatic
+    fun bind(
+        view: UdfpsAccessibilityOverlay,
+        viewModel: UdfpsAccessibilityOverlayViewModel,
+    ) {
+        view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) }
+        view.repeatWhenAttached {
+            // Repeat on CREATED because we update the visibility of the view
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                viewModel.visible.collect { visible -> view.isInvisible = !visible }
+            }
+        }
+    }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
similarity index 71%
copy from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
copy to packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
index 6c1f0fc..7be3230 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package android.companion.virtual.camera;
+package com.android.systemui.deviceentry.ui.view
 
-/**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
- */
-parcelable VirtualCameraMetadata;
+import android.content.Context
+import android.view.View
+
+/** Overlay to handle under-fingerprint sensor accessibility events. */
+class UdfpsAccessibilityOverlay(context: Context?) : View(context)
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
new file mode 100644
index 0000000..80684b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.deviceentry.ui.viewmodel
+
+import android.graphics.Point
+import android.view.MotionEvent
+import android.view.View
+import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
+import com.android.systemui.biometrics.UdfpsUtils
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
+import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+
+/** Models the UI state for the UDFPS accessibility overlay */
+@ExperimentalCoroutinesApi
+class UdfpsAccessibilityOverlayViewModel
+@Inject
+constructor(
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
+    accessibilityInteractor: AccessibilityInteractor,
+    deviceEntryIconViewModel: DeviceEntryIconViewModel,
+    deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel,
+) {
+    private val udfpsUtils = UdfpsUtils()
+    private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
+        udfpsOverlayInteractor.udfpsOverlayParams
+
+    /** Overlay is only visible if touch exploration is enabled and UDFPS can be used. */
+    val visible: Flow<Boolean> =
+        accessibilityInteractor.isTouchExplorationEnabled.flatMapLatest { touchExplorationEnabled ->
+            if (touchExplorationEnabled) {
+                combine(
+                    deviceEntryFgIconViewModel.viewModel,
+                    deviceEntryIconViewModel.deviceEntryViewAlpha,
+                ) { iconViewModel, alpha ->
+                    iconViewModel.type == DeviceEntryIconView.IconType.FINGERPRINT &&
+                        !iconViewModel.useAodVariant &&
+                        alpha == 1f
+                }
+            } else {
+                flowOf(false)
+            }
+        }
+
+    /** Give directional feedback to help the user authenticate with UDFPS. */
+    fun onHoverEvent(v: View, event: MotionEvent): Boolean {
+        val overlayParams = udfpsOverlayParams.value
+        val scaledTouch: Point =
+            udfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, overlayParams)
+
+        if (!udfpsUtils.isWithinSensorArea(event.getPointerId(0), event, overlayParams)) {
+            // view only receives motionEvents when [visible] which requires touchExplorationEnabled
+            val announceStr =
+                udfpsUtils.onTouchOutsideOfSensorArea(
+                    /* touchExplorationEnabled */ true,
+                    v.context,
+                    scaledTouch.x,
+                    scaledTouch.y,
+                    overlayParams,
+                )
+            if (announceStr != null) {
+                v.announceForAccessibility(announceStr)
+            }
+        }
+        // always let the motion events go through to underlying views
+        return false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
index ca4ef0d..4a8c040 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
@@ -34,9 +34,9 @@
 import com.android.systemui.smartspace.SmartspacePrecondition
 import com.android.systemui.smartspace.SmartspaceTargetFilter
 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_DATA_PLUGIN
-import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION
-import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER
 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.LOCKSCREEN_SMARTSPACE_PRECONDITION
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.LOCKSCREEN_SMARTSPACE_TARGET_FILTER
 import com.android.systemui.smartspace.dagger.SmartspaceViewComponent
 import com.android.systemui.util.concurrency.Execution
 import java.util.Optional
@@ -54,8 +54,8 @@
     private val execution: Execution,
     @Main private val uiExecutor: Executor,
     private val smartspaceViewComponentFactory: SmartspaceViewComponent.Factory,
-    @Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
-    @Named(DREAM_SMARTSPACE_TARGET_FILTER)
+    @Named(LOCKSCREEN_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition,
+    @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER)
     private val optionalTargetFilter: Optional<SmartspaceTargetFilter>,
     @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>,
     @Named(DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN)
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 5ec51f4..1540423 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -25,7 +25,9 @@
 import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
@@ -36,20 +38,28 @@
 class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) :
     FlagDependenciesBase(featureFlags, handler) {
     override fun defineDependencies() {
+        // Internal notification backend dependencies
+        crossAppPoliteNotifications dependsOn politeNotifications
+        vibrateWhileUnlockedToken dependsOn politeNotifications
+
+        // Internal notification frontend dependencies
         NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
 
-        val keyguardBottomAreaRefactor = FlagToken(
-                FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
+        // Internal keyguard dependencies
         KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor
 
-        val crossAppPoliteNotifToken =
-                FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
-        val politeNotifToken = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
-        crossAppPoliteNotifToken dependsOn politeNotifToken
-
-        val vibrateWhileUnlockedToken =
-                FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
-        vibrateWhileUnlockedToken dependsOn politeNotifToken
+        // SceneContainer dependencies
+        SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
+        SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW
     }
+
+    private inline val politeNotifications
+        get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
+    private inline val crossAppPoliteNotifications
+        get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
+    private inline val vibrateWhileUnlockedToken: FlagToken
+        get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
+    private inline val keyguardBottomAreaRefactor
+        get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
index e96660f..14fda5e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
@@ -73,9 +73,19 @@
     ) {
         val isMet = !alphaEnabled || betaEnabled
         override fun toString(): String {
-            val isMetBullet = if (isMet) "+" else "-"
-            return "$isMetBullet $alphaName ($alphaEnabled) DEPENDS ON $betaName ($betaEnabled)"
+            val prefix =
+                when {
+                    !isMet -> "  [NOT MET]"
+                    alphaEnabled -> "      [met]"
+                    betaEnabled -> "    [ready]"
+                    else -> "[not ready]"
+                }
+            val alphaState = if (alphaEnabled) "enabled" else "disabled"
+            val betaState = if (betaEnabled) "enabled" else "disabled"
+            return "$prefix $alphaName ($alphaState) DEPENDS ON $betaName ($betaState)"
         }
+        /** Used whe posting a notification of unmet dependencies */
+        fun shortUnmetString(): String = "$alphaName DEPENDS ON $betaName"
     }
 
     protected infix fun UnreleasedFlag.dependsOn(other: UnreleasedFlag) =
@@ -124,7 +134,7 @@
         unmet: List<FlagDependenciesBase.Dependency>
     ) {
         val title = "Invalid flag dependencies: ${unmet.size} of ${all.size}"
-        val details = unmet.joinToString("\n")
+        val details = unmet.joinToString("\n") { it.shortUnmetString() }
         Log.e("FlagDependencies", "$title:\n$details")
         val channel = NotificationChannel("FLAGS", "Flags", NotificationManager.IMPORTANCE_DEFAULT)
         val notification =
@@ -133,6 +143,7 @@
                 .setContentTitle(title)
                 .setContentText(details)
                 .setStyle(Notification.BigTextStyle().bigText(details))
+                .setVisibility(Notification.VISIBILITY_PUBLIC)
                 .build()
         notifManager.createNotificationChannel(channel)
         notifManager.notify("flags", 0, notification)
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index b1d4587..1b35005 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -17,11 +17,11 @@
 
 import android.provider.DeviceConfig
 import com.android.internal.annotations.Keep
-import com.android.systemui.res.R
 import com.android.systemui.flags.FlagsFactory.releasedFlag
 import com.android.systemui.flags.FlagsFactory.resourceBooleanFlag
 import com.android.systemui.flags.FlagsFactory.sysPropBooleanFlag
 import com.android.systemui.flags.FlagsFactory.unreleasedFlag
+import com.android.systemui.res.R
 
 /**
  * List of [Flag] objects for use in SystemUI.
@@ -483,9 +483,6 @@
     // TODO(b/264916608): Tracking Bug
     @JvmField val SCREENSHOT_METADATA = unreleasedFlag("screenshot_metadata")
 
-    // TODO(b/266955521): Tracking bug
-    @JvmField val SCREENSHOT_DETECTION = releasedFlag("screenshot_detection")
-
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
@@ -564,6 +561,13 @@
     val ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS =
         unreleasedFlag("enable_unfold_status_bar_animations")
 
+    // TODO(b/316157842): Tracking Bug
+    // Adds extra delay to notifications measure
+    @Keep
+    @JvmField
+    val ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE =
+        unreleasedFlag("enable_notifications_simulate_slow_measure")
+
     // TODO(b259590361): Tracking bug
     val EXPERIMENTAL_FLAG = unreleasedFlag("exp_flag_release")
 
@@ -581,10 +585,6 @@
     @JvmField
     val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = unreleasedFlag("split_shade_subpixel_optimization")
 
-    // TODO(b/288868056): Tracking Bug
-    @JvmField
-    val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag("pss_task_switcher")
-
     // TODO(b/278761837): Tracking Bug
     @JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(name = "use_new_activity_starter")
 
@@ -600,19 +600,11 @@
     @JvmField
     val LOCKSCREEN_WALLPAPER_DREAM_ENABLED = unreleasedFlag("enable_lockscreen_wallpaper_dream")
 
-    // TODO(b/283084712): Tracking Bug
-    @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag("improved_hun_animations")
-
     // TODO(b/283447257): Tracking bug
     @JvmField
     val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
         unreleasedFlag("bigpicture_notification_lazy_loading")
 
-    // TODO(b/292062937): Tracking bug
-    @JvmField
-    val NOTIFICATION_CLEARABLE_REFACTOR =
-            unreleasedFlag("notification_clearable_refactor")
-
     // TODO(b/283740863): Tracking Bug
     @JvmField
     val ENABLE_NEW_PRIVACY_DIALOG = releasedFlag("enable_new_privacy_dialog")
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 45433e6..3de9e68 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -1054,6 +1054,7 @@
                 return;
             }
             Trace.instantForTrack(Trace.TRACE_TAG_APP, "bugreport", "BugReportAction#onPress");
+            Log.d(TAG, "BugReportAction#onPress");
             // Add a little delay before executing, to give the
             // dialog a chance to go away before it takes a
             // screenshot.
@@ -1069,6 +1070,7 @@
                             Log.w(TAG, "Bugreport handler could not be launched");
                             Trace.instantForTrack(Trace.TRACE_TAG_APP, "bugreport",
                                     "BugReportAction#requestingInteractiveBugReport");
+                            Log.d(TAG, "BugReportAction#requestingInteractiveBugReport");
                             mIActivityManager.requestInteractiveBugReport();
                         }
                     } catch (RemoteException e) {
@@ -1090,6 +1092,7 @@
                 mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
                 Trace.instantForTrack(Trace.TRACE_TAG_APP, "bugreport",
                         "BugReportAction#requestingFullBugReport");
+                Log.d(TAG, "BugReportAction#requestingFullBugReport");
                 mIActivityManager.requestFullBugReport();
             } catch (RemoteException e) {
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
index c490ce7..342325f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
@@ -30,7 +30,7 @@
 import android.os.Binder
 import android.os.Bundle
 import android.util.Log
-import com.android.app.tracing.TraceUtils.Companion.runBlocking
+import com.android.app.tracing.coroutines.runBlocking
 import com.android.systemui.SystemUIAppComponentFactoryBase
 import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
 import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 20da00e..af5d48d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -124,7 +124,7 @@
 
         indicationAreaHandle =
             KeyguardIndicationAreaBinder.bind(
-                notificationShadeWindowView,
+                notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area),
                 keyguardIndicationAreaViewModel,
                 keyguardRootViewModel,
                 indicationController,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 4d60dd0..17d7836 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -626,17 +626,19 @@
             faceAuthLogger.skippingDetection(_isAuthRunning.value, detectCancellationSignal != null)
             return
         }
-        detectCancellationSignal?.cancel()
-        detectCancellationSignal = CancellationSignal()
         withContext(mainDispatcher) {
             // We always want to invoke face detect in the main thread.
             faceAuthLogger.faceDetectionStarted()
-            faceManager?.detectFace(
-                checkNotNull(detectCancellationSignal),
-                detectionCallback,
-                SysUiFaceAuthenticateOptions(currentUserId, uiEvent, uiEvent.extraInfo)
-                    .toFaceAuthenticateOptions()
-            )
+            detectCancellationSignal?.cancel()
+            detectCancellationSignal = CancellationSignal()
+            detectCancellationSignal?.let {
+                faceManager?.detectFace(
+                    it,
+                    detectionCallback,
+                    SysUiFaceAuthenticateOptions(currentUserId, uiEvent, uiEvent.extraInfo)
+                        .toFaceAuthenticateOptions()
+                )
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
index de15fd6..ecf78d5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
@@ -22,15 +22,19 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.data.repository.BiometricType
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.launch
 
 /**
  * Encapsulates business logic for device entry events that impact the side fingerprint sensor
@@ -40,6 +44,7 @@
 class DeviceEntrySideFpsOverlayInteractor
 @Inject
 constructor(
+    @Application private val applicationScope: CoroutineScope,
     @Application private val context: Context,
     deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
@@ -48,7 +53,15 @@
 ) {
 
     init {
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+        if (!DeviceEntryUdfpsRefactor.isEnabled) {
+            applicationScope.launch {
+                deviceEntryFingerprintAuthRepository.availableFpSensorType.collect { sensorType ->
+                    if (sensorType == BiometricType.SIDE_FINGERPRINT) {
+                        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+                    }
+                }
+            }
+        }
     }
 
     private val showIndicatorForPrimaryBouncer: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index 7e360cf..52f2759 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -141,5 +141,6 @@
     companion object {
         val TRANSITION_DURATION_MS = 300.milliseconds
         val TO_GONE_DURATION = 500.milliseconds
+        val TO_AOD_DURATION = TRANSITION_DURATION_MS
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 9fe5c3f..cecc653 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -18,7 +18,7 @@
 
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
-import com.android.app.tracing.TraceUtils.Companion.launch
+import com.android.app.tracing.coroutines.launch
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlags
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 7882a97..3888345 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -22,7 +22,7 @@
 import android.content.Context
 import android.content.Intent
 import android.util.Log
-import com.android.app.tracing.TraceUtils.Companion.withContext
+import com.android.app.tracing.coroutines.withContext
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.animation.Expandable
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index cae6147..f5bcab9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -80,6 +80,11 @@
             return state != GONE
         }
 
+        /** Whether either of the bouncers are visible when we're FINISHED in the given state. */
+        fun isBouncerState(state: KeyguardState): Boolean {
+            return state == PRIMARY_BOUNCER || state == ALTERNATE_BOUNCER
+        }
+
         /**
          * Whether the device is awake ([PowerInteractor.isAwake]) when we're FINISHED in the given
          * keyguard state.
@@ -107,4 +112,4 @@
             return !deviceIsAwakeInState(state)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 64ff3b0c..1277585 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -32,11 +32,8 @@
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
 
 /**
  * Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and
@@ -68,8 +65,6 @@
          * in the range of [0, 1]. View animations should begin and end within a subset of this
          * range. This function maps the [startTime] and [duration] into [0, 1], when this subset is
          * valid.
-         *
-         * Will produce a [SharedFlow], so that identical animations can use the same value.
          */
         fun sharedFlow(
             duration: Duration,
@@ -80,7 +75,7 @@
             onFinish: (() -> Float)? = null,
             interpolator: Interpolator = LINEAR,
             name: String? = null
-        ): SharedFlow<Float> {
+        ): Flow<Float> {
             if (!duration.isPositive()) {
                 throw IllegalArgumentException("duration must be a positive number: $duration")
             }
@@ -137,7 +132,6 @@
                     value
                 }
                 .filterNotNull()
-                .shareIn(scope, SharingStarted.WhileSubscribed())
         }
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt
deleted file mode 100644
index ebf1beb..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt
+++ /dev/null
@@ -1,25 +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.keyguard.ui.adapter
-
-/**
- * Temporary adapter class while
- * [com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController] is being refactored
- * before [com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy] is removed.
- *
- * TODO (b/278719514): Delete once udfps keyguard view is fully refactored.
- */
-interface UdfpsKeyguardViewControllerAdapter
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
new file mode 100644
index 0000000..c749818
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.content.res.ColorStateList
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+@ExperimentalCoroutinesApi
+object AlternateBouncerUdfpsViewBinder {
+
+    /** Updates UI for the UDFPS icon on the alternate bouncer. */
+    @JvmStatic
+    fun bind(
+        view: DeviceEntryIconView,
+        viewModel: AlternateBouncerUdfpsIconViewModel,
+    ) {
+        if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) {
+            return
+        }
+        val fgIconView = view.iconView
+        val bgView = view.bgView
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.accessibilityDelegateHint.collect { hint ->
+                    view.accessibilityHintType = hint
+                }
+            }
+        }
+
+        fgIconView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.fgViewModel.collect { fgViewModel ->
+                    fgIconView.setImageState(
+                        view.getIconState(fgViewModel.type, fgViewModel.useAodVariant),
+                        /* merge */ false
+                    )
+                    fgIconView.imageTintList = ColorStateList.valueOf(fgViewModel.tint)
+                    fgIconView.setPadding(
+                        fgViewModel.padding,
+                        fgViewModel.padding,
+                        fgViewModel.padding,
+                        fgViewModel.padding,
+                    )
+                }
+            }
+        }
+
+        bgView.visibility = View.VISIBLE
+        bgView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.bgColor.collect { color ->
+                        bgView.imageTintList = ColorStateList.valueOf(color)
+                    }
+                }
+                launch { viewModel.bgAlpha.collect { alpha -> bgView.alpha = alpha } }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index d1d323f..78906ac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -17,47 +17,49 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.view.View
-import android.view.ViewGroup
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.classifier.Classifier
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.scrim.ScrimView
-import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.gesture.TapGestureDetector
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 
-/** Binds the alternate bouncer view to its view-model. */
+/**
+ * Binds the alternate bouncer view to its view-model.
+ *
+ * For devices that support UDFPS, this includes a UDFPS view.
+ */
 @ExperimentalCoroutinesApi
 object AlternateBouncerViewBinder {
 
     /** Binds the view to the view-model, continuing to update the former based on the latter. */
     @JvmStatic
     fun bind(
-        view: ViewGroup,
+        view: ConstraintLayout,
         viewModel: AlternateBouncerViewModel,
-        scope: CoroutineScope,
-        notificationShadeWindowController: NotificationShadeWindowController,
         falsingManager: FalsingManager,
         swipeUpAnywhereGestureHandler: SwipeUpAnywhereGestureHandler,
         tapGestureDetector: TapGestureDetector,
+        alternateBouncerUdfpsIconViewModel: AlternateBouncerUdfpsIconViewModel,
     ) {
-        if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) return
-        scope.launch {
-            // forcePluginOpen is necessary to show over occluded apps.
-            // This cannot be tied to the view's lifecycle because setting this allows the view
-            // to be started in the first place.
-            viewModel.forcePluginOpen.collect {
-                notificationShadeWindowController.setForcePluginOpen(it, this)
-            }
+        if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) {
+            return
         }
+        optionallyAddUdfpsView(
+            view = view,
+            alternateBouncerUdfpsIconViewModel = alternateBouncerUdfpsIconViewModel,
+        )
 
         val scrim = view.requireViewById(R.id.alternate_bouncer_scrim) as ScrimView
         view.repeatWhenAttached { alternateBouncerViewContainer ->
@@ -99,6 +101,52 @@
             }
         }
     }
+
+    private fun optionallyAddUdfpsView(
+        view: ConstraintLayout,
+        alternateBouncerUdfpsIconViewModel: AlternateBouncerUdfpsIconViewModel,
+    ) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    alternateBouncerUdfpsIconViewModel.iconLocation.collect { iconLocation ->
+                        val viewId = R.id.alternate_bouncer_udfps_icon_view
+                        var udfpsView = view.getViewById(viewId)
+                        if (udfpsView == null) {
+                            udfpsView =
+                                DeviceEntryIconView(view.context, null).apply { id = viewId }
+                            view.addView(udfpsView)
+                            AlternateBouncerUdfpsViewBinder.bind(
+                                udfpsView,
+                                alternateBouncerUdfpsIconViewModel,
+                            )
+                        }
+
+                        val constraintSet = ConstraintSet().apply { clone(view) }
+                        constraintSet.apply {
+                            constrainWidth(viewId, iconLocation.width)
+                            constrainHeight(viewId, iconLocation.height)
+                            connect(
+                                viewId,
+                                ConstraintSet.TOP,
+                                ConstraintSet.PARENT_ID,
+                                ConstraintSet.TOP,
+                                iconLocation.top,
+                            )
+                            connect(
+                                viewId,
+                                ConstraintSet.START,
+                                ConstraintSet.PARENT_ID,
+                                ConstraintSet.START,
+                                iconLocation.left
+                            )
+                        }
+                        constraintSet.applyTo(view)
+                    }
+                }
+            }
+        }
+    }
 }
 
 private const val swipeTag = "AlternateBouncer-SWIPE"
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
index dcf4284..a02e8ac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -131,10 +131,10 @@
 
         bgView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch { bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha } }
                 launch {
-                    bgViewModel.viewModel.collect { bgViewModel ->
-                        bgView.alpha = bgViewModel.alpha
-                        bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint)
+                    bgViewModel.color.collect { color ->
+                        bgView.imageTintList = ColorStateList.valueOf(color)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 7d290c3..05fe0b2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -32,8 +32,6 @@
 import com.android.systemui.res.R
 import kotlinx.coroutines.launch
 
-private val TAG = KeyguardClockViewBinder::class.simpleName
-
 object KeyguardClockViewBinder {
     @JvmStatic
     fun bind(
@@ -74,12 +72,6 @@
                         applyConstraints(clockSection, keyguardRootView, true)
                     }
                 }
-                launch {
-                    if (!migrateClocksToBlueprint()) return@launch
-                    viewModel.hasCustomWeatherDataDisplay.collect {
-                        applyConstraints(clockSection, keyguardRootView, true)
-                    }
-                }
             }
         }
     }
@@ -132,7 +124,7 @@
     fun applyConstraints(
         clockSection: ClockSection,
         rootView: ConstraintLayout,
-        animated: Boolean
+        animated: Boolean,
     ) {
         val constraintSet = ConstraintSet().apply { clone(rootView) }
         clockSection.applyConstraints(constraintSet)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 4efd9ef..4c33d90 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -54,12 +54,11 @@
         keyguardRootViewModel: KeyguardRootViewModel,
         indicationController: KeyguardIndicationController,
     ): DisposableHandle {
-        val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area)
-        indicationController.setIndicationArea(indicationArea)
+        indicationController.setIndicationArea(view)
 
-        val indicationText: TextView = indicationArea.requireViewById(R.id.keyguard_indication_text)
+        val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
         val indicationTextBottom: TextView =
-            indicationArea.requireViewById(R.id.keyguard_indication_text_bottom)
+            view.requireViewById(R.id.keyguard_indication_text_bottom)
 
         view.clipChildren = false
         view.clipToPadding = false
@@ -71,7 +70,7 @@
                     launch {
                         if (keyguardBottomAreaRefactor()) {
                             keyguardRootViewModel.alpha.collect { alpha ->
-                                indicationArea.apply {
+                                view.apply {
                                     this.importantForAccessibility =
                                         if (alpha == 0f) {
                                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
@@ -83,7 +82,7 @@
                             }
                         } else {
                             viewModel.alpha.collect { alpha ->
-                                indicationArea.apply {
+                                view.apply {
                                     this.importantForAccessibility =
                                         if (alpha == 0f) {
                                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
@@ -98,7 +97,7 @@
 
                     launch {
                         viewModel.indicationAreaTranslationX.collect { translationX ->
-                            indicationArea.translationX = translationX
+                            view.translationX = translationX
                         }
                     }
 
@@ -113,9 +112,7 @@
                                     0
                                 }
                             }
-                            .collect { paddingPx ->
-                                indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
-                            }
+                            .collect { paddingPx -> view.setPadding(paddingPx, 0, paddingPx, 0) }
                     }
 
                     launch {
@@ -124,7 +121,7 @@
                             .flatMapLatest { defaultBurnInOffsetY ->
                                 viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
                             }
-                            .collect { translationY -> indicationArea.translationY = translationY }
+                            .collect { translationY -> view.translationY = translationY }
                     }
 
                     launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 01a1ca3..fad0370 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -29,7 +29,6 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
 import com.android.internal.jank.InteractionJankMonitor
@@ -67,6 +66,7 @@
 import javax.inject.Provider
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -205,7 +205,6 @@
                                     childViews[aodNotificationIconContainerId]
                                         ?.setAodNotifIconContainerIsVisible(
                                             isVisible,
-                                            featureFlags,
                                             iconsAppearTranslationPx.value,
                                             screenOffAnimationController,
                                         )
@@ -256,6 +255,7 @@
                                 vibratorHelper.performHapticFeedback(
                                     view,
                                     HapticFeedbackConstants.CONFIRM,
+                                    HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
                                 )
                             }
                         }
@@ -265,6 +265,7 @@
                                 vibratorHelper.performHapticFeedback(
                                     view,
                                     HapticFeedbackConstants.REJECT,
+                                    HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
                                 )
                             }
                         }
@@ -359,41 +360,32 @@
         }
     }
 
-    @JvmStatic
-    fun bindAodIconVisibility(
+    suspend fun bindAodNotifIconVisibility(
         view: View,
         isVisible: Flow<AnimatedValue<Boolean>>,
         configuration: ConfigurationState,
-        featureFlags: FeatureFlagsClassic,
         screenOffAnimationController: ScreenOffAnimationController,
-    ): DisposableHandle? {
+    ) {
         KeyguardShadeMigrationNssl.assertInLegacyMode()
-        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return null
-        return view.repeatWhenAttached {
-            lifecycleScope.launch {
-                val iconAppearTranslationPx =
-                    configuration
-                        .getDimensionPixelSize(R.dimen.shelf_appear_translation)
-                        .stateIn(this)
-                isVisible.collect { isVisible ->
-                    view.setAodNotifIconContainerIsVisible(
-                        isVisible,
-                        featureFlags,
-                        iconAppearTranslationPx.value,
-                        screenOffAnimationController,
-                    )
-                }
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return
+        coroutineScope {
+            val iconAppearTranslationPx =
+                configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
+            isVisible.collect { isVisible ->
+                view.setAodNotifIconContainerIsVisible(
+                    isVisible = isVisible,
+                    iconsAppearTranslationPx = iconAppearTranslationPx.value,
+                    screenOffAnimationController = screenOffAnimationController,
+                )
             }
         }
     }
 
     private fun View.setAodNotifIconContainerIsVisible(
         isVisible: AnimatedValue<Boolean>,
-        featureFlags: FeatureFlagsClassic,
         iconsAppearTranslationPx: Int,
         screenOffAnimationController: ScreenOffAnimationController,
     ) {
-        val statusViewMigrated = KeyguardShadeMigrationNssl.isEnabled
         animate().cancel()
         val animatorListener =
             object : AnimatorListenerAdapter() {
@@ -404,13 +396,13 @@
         when {
             !isVisible.isAnimating -> {
                 alpha = 1f
-                if (!statusViewMigrated) {
+                if (!KeyguardShadeMigrationNssl.isEnabled) {
                     translationY = 0f
                 }
                 visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE
             }
             newAodTransition() -> {
-                animateInIconTranslation(statusViewMigrated)
+                animateInIconTranslation()
                 if (isVisible.value) {
                     CrossFadeHelper.fadeIn(this, animatorListener)
                 } else {
@@ -419,7 +411,7 @@
             }
             !isVisible.value -> {
                 // Let's make sure the icon are translated to 0, since we cancelled it above
-                animateInIconTranslation(statusViewMigrated)
+                animateInIconTranslation()
                 CrossFadeHelper.fadeOut(this, animatorListener)
             }
             visibility != View.VISIBLE -> {
@@ -429,13 +421,12 @@
                 appearIcons(
                     animate = screenOffAnimationController.shouldAnimateAodIcons(),
                     iconsAppearTranslationPx,
-                    statusViewMigrated,
                     animatorListener,
                 )
             }
             else -> {
                 // Let's make sure the icons are translated to 0, since we cancelled it above
-                animateInIconTranslation(statusViewMigrated)
+                animateInIconTranslation()
                 // We were fading out, let's fade in instead
                 CrossFadeHelper.fadeIn(this, animatorListener)
             }
@@ -445,11 +436,10 @@
     private fun View.appearIcons(
         animate: Boolean,
         iconAppearTranslation: Int,
-        statusViewMigrated: Boolean,
         animatorListener: Animator.AnimatorListener,
     ) {
         if (animate) {
-            if (!statusViewMigrated) {
+            if (!KeyguardShadeMigrationNssl.isEnabled) {
                 translationY = -iconAppearTranslation.toFloat()
             }
             alpha = 0f
@@ -457,19 +447,19 @@
                 .alpha(1f)
                 .setInterpolator(Interpolators.LINEAR)
                 .setDuration(AOD_ICONS_APPEAR_DURATION)
-                .apply { if (statusViewMigrated) animateInIconTranslation() }
+                .apply { if (KeyguardShadeMigrationNssl.isEnabled) animateInIconTranslation() }
                 .setListener(animatorListener)
                 .start()
         } else {
             alpha = 1.0f
-            if (!statusViewMigrated) {
+            if (!KeyguardShadeMigrationNssl.isEnabled) {
                 translationY = 0f
             }
         }
     }
 
-    private fun View.animateInIconTranslation(statusViewMigrated: Boolean) {
-        if (!statusViewMigrated) {
+    private fun View.animateInIconTranslation() {
+        if (!KeyguardShadeMigrationNssl.isEnabled) {
             animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 954d2cf..e36819b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -16,13 +16,19 @@
 
 package com.android.systemui.keyguard.ui.binder
 
+import android.transition.TransitionManager
+import android.view.View
+import androidx.constraintlayout.helper.widget.Layer
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import kotlinx.coroutines.launch
 
 object KeyguardSmartspaceViewBinder {
     @JvmStatic
@@ -30,15 +36,63 @@
         smartspaceSection: SmartspaceSection,
         keyguardRootView: ConstraintLayout,
         clockViewModel: KeyguardClockViewModel,
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
     ) {
         keyguardRootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
-                clockViewModel.hasCustomWeatherDataDisplay.collect {
-                    val constraintSet = ConstraintSet().apply { clone(keyguardRootView) }
-                    smartspaceSection.applyConstraints(constraintSet)
-                    constraintSet.applyTo(keyguardRootView)
+                launch {
+                    clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
+                        ->
+                        if (hasCustomWeatherDataDisplay) {
+                            removeDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
+                        } else {
+                            addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
+                        }
+                        clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+                        val constraintSet = ConstraintSet().apply { clone(keyguardRootView) }
+                        smartspaceSection.applyConstraints(constraintSet)
+                        TransitionManager.beginDelayedTransition(keyguardRootView)
+                        constraintSet.applyTo(keyguardRootView)
+                    }
                 }
             }
         }
     }
+
+    private fun addDateWeatherToBurnInLayer(
+        constraintLayout: ConstraintLayout,
+        smartspaceViewModel: KeyguardSmartspaceViewModel
+    ) {
+        val burnInLayer = constraintLayout.requireViewById<Layer>(R.id.burn_in_layer)
+        burnInLayer.apply {
+            if (
+                smartspaceViewModel.isSmartspaceEnabled &&
+                    smartspaceViewModel.isDateWeatherDecoupled
+            ) {
+                val dateView = constraintLayout.requireViewById<View>(smartspaceViewModel.dateId)
+                val weatherView =
+                    constraintLayout.requireViewById<View>(smartspaceViewModel.weatherId)
+                addView(weatherView)
+                addView(dateView)
+            }
+        }
+    }
+
+    private fun removeDateWeatherToBurnInLayer(
+        constraintLayout: ConstraintLayout,
+        smartspaceViewModel: KeyguardSmartspaceViewModel
+    ) {
+        val burnInLayer = constraintLayout.requireViewById<Layer>(R.id.burn_in_layer)
+        burnInLayer.apply {
+            if (
+                smartspaceViewModel.isSmartspaceEnabled &&
+                    smartspaceViewModel.isDateWeatherDecoupled
+            ) {
+                val dateView = smartspaceViewModel.dateView
+                val weatherView = smartspaceViewModel.weatherView
+                removeView(weatherView)
+                removeView(dateView)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
deleted file mode 100644
index 52d87d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
+++ /dev/null
@@ -1,61 +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.keyguard.ui.binder
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsAodFingerprintViewBinder {
-
-    /**
-     * Drives UI for the UDFPS aod fingerprint view. See [UdfpsFingerprintViewBinder] and
-     * [UdfpsBackgroundViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: LottieAnimationView,
-        viewModel: UdfpsAodViewModel,
-    ) {
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.burnInOffsets.collect { burnInOffsets ->
-                        view.progress = burnInOffsets.progress
-                        view.translationX = burnInOffsets.x.toFloat()
-                        view.translationY = burnInOffsets.y.toFloat()
-                    }
-                }
-
-                launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } }
-
-                launch {
-                    viewModel.padding.collect { padding ->
-                        view.setPadding(padding, padding, padding, padding)
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
deleted file mode 100644
index 0113628..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
+++ /dev/null
@@ -1,55 +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.keyguard.ui.binder
-
-import android.content.res.ColorStateList
-import android.widget.ImageView
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsBackgroundViewBinder {
-
-    /**
-     * Drives UI for the udfps background view. See [UdfpsAodFingerprintViewBinder] and
-     * [UdfpsFingerprintViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: ImageView,
-        viewModel: BackgroundViewModel,
-    ) {
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.transition.collect {
-                        view.alpha = it.alpha
-                        view.scaleX = it.scale
-                        view.scaleY = it.scale
-                        view.imageTintList = ColorStateList.valueOf(it.color)
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
deleted file mode 100644
index d4621e6..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.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.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.model.KeyPath
-import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsFingerprintViewBinder {
-    private var udfpsIconColor = 0
-
-    /**
-     * Drives UI for the UDFPS fingerprint view when it's NOT on aod. See
-     * [UdfpsAodFingerprintViewBinder] and [UdfpsBackgroundViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: LottieAnimationView,
-        viewModel: FingerprintViewModel,
-    ) {
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch {
-                    viewModel.transition.collect {
-                        view.alpha = it.alpha
-                        view.scaleX = it.scale
-                        view.scaleY = it.scale
-                        if (udfpsIconColor != (it.color)) {
-                            udfpsIconColor = it.color
-                            view.invalidate()
-                        }
-                    }
-                }
-
-                launch {
-                    viewModel.burnInOffsets.collect { burnInOffsets ->
-                        view.translationX = burnInOffsets.x.toFloat()
-                        view.translationY = burnInOffsets.y.toFloat()
-                    }
-                }
-
-                launch {
-                    viewModel.dozeAmount.collect { dozeAmount ->
-                        // Lottie progress represents: aod=0 to lockscreen=1
-                        view.progress = 1f - dozeAmount
-                    }
-                }
-
-                launch {
-                    viewModel.padding.collect { padding ->
-                        view.setPadding(padding, padding, padding, padding)
-                    }
-                }
-            }
-        }
-
-        // Add a callback that updates the color to `udfpsIconColor` whenever invalidate is called
-        view.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER) {
-            PorterDuffColorFilter(udfpsIconColor, PorterDuff.Mode.SRC_ATOP)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
deleted file mode 100644
index aabb3f4..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
+++ /dev/null
@@ -1,55 +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.biometrics.ui.binder
-
-import android.view.View
-import com.android.systemui.res.R
-import com.android.systemui.keyguard.ui.binder.UdfpsAodFingerprintViewBinder
-import com.android.systemui.keyguard.ui.binder.UdfpsBackgroundViewBinder
-import com.android.systemui.keyguard.ui.binder.UdfpsFingerprintViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
-import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-object UdfpsKeyguardInternalViewBinder {
-
-    @JvmStatic
-    fun bind(
-        view: View,
-        viewModel: UdfpsKeyguardInternalViewModel,
-        aodViewModel: UdfpsAodViewModel,
-        fingerprintViewModel: FingerprintViewModel,
-        backgroundViewModel: BackgroundViewModel,
-    ) {
-        view.accessibilityDelegate = viewModel.accessibilityDelegate
-
-        // bind child views
-        UdfpsAodFingerprintViewBinder.bind(view.requireViewById(R.id.udfps_aod_fp), aodViewModel)
-        UdfpsFingerprintViewBinder.bind(
-            view.requireViewById(R.id.udfps_lockscreen_fp),
-            fingerprintViewModel
-        )
-        UdfpsBackgroundViewBinder.bind(
-            view.requireViewById(R.id.udfps_keyguard_fp_bg),
-            backgroundViewModel
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt
deleted file mode 100644
index 475d26f..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt
+++ /dev/null
@@ -1,111 +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.keyguard.ui.binder
-
-import android.graphics.RectF
-import android.view.View
-import android.widget.FrameLayout
-import androidx.asynclayoutinflater.view.AsyncLayoutInflater
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.biometrics.UdfpsKeyguardView
-import com.android.systemui.biometrics.ui.binder.UdfpsKeyguardInternalViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
-import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.res.R
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-@ExperimentalCoroutinesApi
-object UdfpsKeyguardViewBinder {
-    /**
-     * Drives UI for the keyguard UDFPS view. Inflates child views on a background thread. For view
-     * binders for its child views, see [UdfpsFingerprintViewBinder], [UdfpsBackgroundViewBinder] &
-     * [UdfpsAodFingerprintViewBinder].
-     */
-    @JvmStatic
-    fun bind(
-        view: UdfpsKeyguardView,
-        viewModel: UdfpsKeyguardViewModel,
-        udfpsKeyguardInternalViewModel: UdfpsKeyguardInternalViewModel,
-        aodViewModel: UdfpsAodViewModel,
-        fingerprintViewModel: FingerprintViewModel,
-        backgroundViewModel: BackgroundViewModel,
-    ) {
-        val layoutInflaterFinishListener =
-            AsyncLayoutInflater.OnInflateFinishedListener { inflatedInternalView, _, parent ->
-                UdfpsKeyguardInternalViewBinder.bind(
-                    inflatedInternalView,
-                    udfpsKeyguardInternalViewModel,
-                    aodViewModel,
-                    fingerprintViewModel,
-                    backgroundViewModel,
-                )
-                val lp = inflatedInternalView.layoutParams as FrameLayout.LayoutParams
-                lp.width = viewModel.sensorBounds.width()
-                lp.height = viewModel.sensorBounds.height()
-                val relativeToView =
-                    getBoundsRelativeToView(
-                        inflatedInternalView,
-                        RectF(viewModel.sensorBounds),
-                    )
-                lp.setMarginsRelative(
-                    relativeToView.left.toInt(),
-                    relativeToView.top.toInt(),
-                    relativeToView.right.toInt(),
-                    relativeToView.bottom.toInt(),
-                )
-                parent!!.addView(inflatedInternalView, lp)
-            }
-        val inflater = AsyncLayoutInflater(view.context)
-        inflater.inflate(R.layout.udfps_keyguard_view_internal, view, layoutInflaterFinishListener)
-
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.CREATED) {
-                launch {
-                    combine(aodViewModel.isVisible, fingerprintViewModel.visible) {
-                            isAodVisible,
-                            isFingerprintVisible ->
-                            isAodVisible || isFingerprintVisible
-                        }
-                        .collect { view.setVisible(it) }
-                }
-            }
-        }
-    }
-
-    /**
-     * Converts coordinates of RectF relative to the screen to coordinates relative to this view.
-     *
-     * @param bounds RectF based off screen coordinates in current orientation
-     */
-    private fun getBoundsRelativeToView(view: View, bounds: RectF): RectF {
-        val pos: IntArray = view.locationOnScreen
-        return RectF(
-            bounds.left - pos[0],
-            bounds.top - pos[1],
-            bounds.right - pos[0],
-            bounds.bottom - pos[1]
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index 9d557bb..920fc04 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.keyguard.ui.transitions
 
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToAodTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
@@ -39,6 +41,18 @@
 abstract class DeviceEntryIconTransitionModule {
     @Binds
     @IntoSet
+    abstract fun alternateBouncerToAod(
+        impl: AlternateBouncerToAodTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
+    abstract fun alternateBouncerToGone(
+        impl: AlternateBouncerToGoneTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
     abstract fun aodToLockscreen(
         impl: AodToLockscreenTransitionViewModel
     ): DeviceEntryIconTransition
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 1c6a2ab..bc9671e 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
@@ -31,18 +31,21 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
 import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import java.util.Optional
 import javax.inject.Inject
 import javax.inject.Named
 import kotlin.jvm.optionals.getOrNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
  * Positions elements of the lockscreen to the default position.
  *
  * This will be the most common use case for phones in portrait mode.
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 @JvmSuppressWildcards
 class DefaultKeyguardBlueprint
@@ -62,6 +65,7 @@
     communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
     clockSection: ClockSection,
     smartspaceSection: SmartspaceSection,
+    udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
 ) : KeyguardBlueprint {
     override val id: String = DEFAULT
 
@@ -79,7 +83,8 @@
             aodBurnInSection,
             communalTutorialIndicatorSection,
             clockSection,
-            defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views.
+            defaultDeviceEntrySection,
+            udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
         )
 
     companion object {
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 bf70682..9b40433 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
@@ -29,14 +29,17 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
 import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.util.kotlin.getOrNull
 import java.util.Optional
 import javax.inject.Inject
 import javax.inject.Named
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /** Vertically aligns the shortcuts with the udfps. */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class ShortcutsBesideUdfpsKeyguardBlueprint
 @Inject
@@ -53,6 +56,7 @@
     defaultNotificationStackScrollLayoutSection: DefaultNotificationStackScrollLayoutSection,
     aodNotificationIconsSection: AodNotificationIconsSection,
     aodBurnInSection: AodBurnInSection,
+    udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
 ) : KeyguardBlueprint {
     override val id: String = SHORTCUTS_BESIDE_UDFPS
 
@@ -68,7 +72,8 @@
             splitShadeGuidelines,
             aodNotificationIconsSection,
             aodBurnInSection,
-            defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views.
+            defaultDeviceEntrySection,
+            udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
index f890ae6..16539db 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
@@ -30,6 +30,8 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
 import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
+import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection
 import com.android.systemui.util.kotlin.getOrNull
@@ -59,6 +61,8 @@
     aodNotificationIconsSection: AodNotificationIconsSection,
     aodBurnInSection: AodBurnInSection,
     communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
+    smartspaceSection: SmartspaceSection,
+    clockSection: SplitShadeClockSection,
 ) : KeyguardBlueprint {
     override val id: String = ID
 
@@ -73,8 +77,10 @@
             splitShadeNotificationStackScrollLayoutSection,
             splitShadeGuidelines,
             aodNotificationIconsSection,
+            smartspaceSection,
             aodBurnInSection,
             communalTutorialIndicatorSection,
+            clockSection,
             defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views.
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index 8166b45..d89e1e4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -23,7 +23,6 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
@@ -38,7 +37,6 @@
     private val context: Context,
     private val clockViewModel: KeyguardClockViewModel,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
-    private val featureFlags: FeatureFlagsClassic,
 ) : KeyguardSection() {
     lateinit var burnInLayer: Layer
 
@@ -59,6 +57,8 @@
                 }
             }
         if (migrateClocksToBlueprint()) {
+            // weather and date parts won't be added here, cause their visibility doesn't align
+            // with others in burnInLayer
             addSmartspaceViews(constraintLayout)
         }
         constraintLayout.addView(burnInLayer)
@@ -89,14 +89,6 @@
                 val smartspaceView =
                     constraintLayout.requireViewById<View>(smartspaceViewModel.smartspaceViewId)
                 addView(smartspaceView)
-                if (smartspaceViewModel.isDateWeatherDecoupled) {
-                    val dateView =
-                        constraintLayout.requireViewById<View>(smartspaceViewModel.dateId)
-                    val weatherView =
-                        constraintLayout.requireViewById<View>(smartspaceViewModel.weatherId)
-                    addView(weatherView)
-                    addView(dateView)
-                }
             }
         }
     }
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 39a0547..b429ab4 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
@@ -28,7 +28,6 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
@@ -49,7 +48,6 @@
 constructor(
     private val context: Context,
     private val configurationState: ConfigurationState,
-    private val featureFlags: FeatureFlagsClassic,
     private val iconBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
     private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
@@ -119,14 +117,8 @@
             }
         constraintSet.apply {
             if (migrateClocksToBlueprint()) {
-                connect(
-                    nicId,
-                    TOP,
-                    smartspaceViewModel.smartspaceViewId,
-                    topAlignment,
-                    bottomMargin
-                )
-                setGoneMargin(nicId, topAlignment, bottomMargin)
+                connect(nicId, TOP, smartspaceViewModel.smartspaceViewId, BOTTOM, bottomMargin)
+                setGoneMargin(nicId, BOTTOM, bottomMargin)
             } else {
                 connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index 1df920a..656c75c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -27,7 +27,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
-import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder
@@ -50,19 +50,21 @@
     alpha: Float,
 ) = views.forEach { view -> this.setAlpha(view.id, alpha) }
 
-class ClockSection
+open class ClockSection
 @Inject
 constructor(
     private val clockInteractor: KeyguardClockInteractor,
-    private val keyguardClockViewModel: KeyguardClockViewModel,
-    val smartspaceViewModel: KeyguardSmartspaceViewModel,
+    protected val keyguardClockViewModel: KeyguardClockViewModel,
+    private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     private val context: Context,
     private val splitShadeStateController: SplitShadeStateController,
-    private val featureFlags: FeatureFlagsClassic,
 ) : KeyguardSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {}
 
     override fun bindData(constraintLayout: ConstraintLayout) {
+        if (!Flags.migrateClocksToBlueprint()) {
+            return
+        }
         KeyguardClockViewBinder.bind(
             this,
             constraintLayout,
@@ -72,6 +74,9 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
+        if (!Flags.migrateClocksToBlueprint()) {
+            return
+        }
         clockInteractor.clock?.let { clock ->
             constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet))
         }
@@ -94,16 +99,6 @@
         }
     }
 
-    var largeClockEndGuideline = PARENT_ID
-
-    // Return if largeClockEndGuideline changes,
-    // and use it to decide whether to refresh blueprint
-    fun setClockShouldBeCentered(shouldBeCentered: Boolean): Boolean {
-        val previousValue = largeClockEndGuideline
-        largeClockEndGuideline = if (shouldBeCentered) PARENT_ID else R.id.split_shade_guideline
-        return previousValue != largeClockEndGuideline
-    }
-
     private fun getTargetClockFace(clock: ClockController): ClockFaceLayout =
         if (keyguardClockViewModel.useLargeClock) getLargeClockFace(clock)
         else getSmallClockFace(clock)
@@ -113,10 +108,10 @@
 
     private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout
     private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout
-    fun applyDefaultConstraints(constraints: ConstraintSet) {
+    open fun applyDefaultConstraints(constraints: ConstraintSet) {
         constraints.apply {
             connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START)
-            connect(R.id.lockscreen_clock_view_large, END, largeClockEndGuideline, END)
+            connect(R.id.lockscreen_clock_view_large, END, PARENT_ID, END)
             connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
             var largeClockTopMargin =
                 context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index a693ec9..0bf9ad0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -23,7 +23,6 @@
 import android.util.DisplayMetrics
 import android.view.View
 import android.view.WindowManager
-import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
@@ -32,31 +31,24 @@
 import com.android.keyguard.LockIconViewController
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.biometrics.AuthController
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 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.SwipeUpAnywhereGestureHandler
-import com.android.systemui.keyguard.ui.binder.AlternateBouncerViewBinder
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
-import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
 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.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.gesture.TapGestureDetector
 import dagger.Lazy
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-/** Includes both the device entry icon and the alternate bouncer scrim. */
+/** Includes the device entry icon. */
 @ExperimentalCoroutinesApi
 class DefaultDeviceEntrySection
 @Inject
@@ -72,27 +64,15 @@
     private val deviceEntryForegroundViewModel: Lazy<DeviceEntryForegroundViewModel>,
     private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>,
     private val falsingManager: Lazy<FalsingManager>,
-    private val alternateBouncerViewModel: Lazy<AlternateBouncerViewModel>,
-    private val notificationShadeWindowController: Lazy<NotificationShadeWindowController>,
-    @Application private val scope: CoroutineScope,
-    private val swipeUpAnywhereGestureHandler: Lazy<SwipeUpAnywhereGestureHandler>,
-    private val tapGestureDetector: Lazy<TapGestureDetector>,
     private val vibratorHelper: Lazy<VibratorHelper>,
 ) : KeyguardSection() {
     private val deviceEntryIconViewId = R.id.device_entry_icon_view
-    private val alternateBouncerViewId = R.id.alternate_bouncer
 
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) {
             return
         }
 
-        if (DeviceEntryUdfpsRefactor.isEnabled) {
-            // The alternate bouncer scrim needs to be below the device entry icon view, so
-            // we add the view here before adding the device entry icon view.
-            View.inflate(context, R.layout.alternate_bouncer, constraintLayout)
-        }
-
         notificationPanelView.findViewById<View>(R.id.lock_icon_view).let {
             notificationPanelView.removeView(it)
         }
@@ -119,17 +99,6 @@
                     vibratorHelper.get(),
                 )
             }
-            constraintLayout.findViewById<FrameLayout?>(alternateBouncerViewId)?.let {
-                AlternateBouncerViewBinder.bind(
-                    it,
-                    alternateBouncerViewModel.get(),
-                    scope,
-                    notificationShadeWindowController.get(),
-                    falsingManager.get(),
-                    swipeUpAnywhereGestureHandler.get(),
-                    tapGestureDetector.get(),
-                )
-            }
         } else {
             constraintLayout.findViewById<LockIconView?>(R.id.lock_icon_view)?.let {
                 lockIconViewController.get().setLockIconView(it)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 56f717d..66c137f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -54,7 +54,7 @@
         if (keyguardBottomAreaRefactor()) {
             indicationAreaHandle =
                 KeyguardIndicationAreaBinder.bind(
-                    constraintLayout,
+                    constraintLayout.requireViewById(R.id.keyguard_indication_area),
                     keyguardIndicationAreaViewModel,
                     keyguardRootViewModel,
                     indicationController,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
new file mode 100644
index 0000000..e1a33de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.layout.sections
+
+import android.content.Context
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.Flags
+import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
+import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
+import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Positions the UDFPS accessibility overlay on the bottom half of the keyguard. */
+@ExperimentalCoroutinesApi
+class DefaultUdfpsAccessibilityOverlaySection
+@Inject
+constructor(
+    private val context: Context,
+    private val viewModel: UdfpsAccessibilityOverlayViewModel,
+) : KeyguardSection() {
+    private val viewId = R.id.udfps_accessibility_overlay
+    private var cachedConstraintLayout: ConstraintLayout? = null
+
+    override fun addViews(constraintLayout: ConstraintLayout) {
+        cachedConstraintLayout = constraintLayout
+        constraintLayout.addView(UdfpsAccessibilityOverlay(context).apply { id = viewId })
+    }
+
+    override fun bindData(constraintLayout: ConstraintLayout) {
+        UdfpsAccessibilityOverlayBinder.bind(
+            constraintLayout.findViewById(viewId)!!,
+            viewModel,
+        )
+    }
+
+    override fun applyConstraints(constraintSet: ConstraintSet) {
+        constraintSet.apply {
+            connect(viewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+            connect(viewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
+
+            create(R.id.udfps_accessibility_overlay_top_guideline, ConstraintSet.HORIZONTAL)
+            setGuidelinePercent(R.id.udfps_accessibility_overlay_top_guideline, .5f)
+            connect(
+                viewId,
+                ConstraintSet.TOP,
+                R.id.udfps_accessibility_overlay_top_guideline,
+                ConstraintSet.BOTTOM,
+            )
+
+            if (Flags.keyguardBottomAreaRefactor()) {
+                connect(
+                    viewId,
+                    ConstraintSet.BOTTOM,
+                    R.id.keyguard_indication_area,
+                    ConstraintSet.TOP,
+                )
+            } else {
+                connect(viewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+            }
+        }
+    }
+
+    override fun removeViews(constraintLayout: ConstraintLayout) {
+        constraintLayout.removeView(viewId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index 368b388..8cd51cd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.ui.view.layout.sections
 
 import android.content.Context
-import android.view.View
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -36,7 +35,7 @@
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
 import javax.inject.Inject
 
-class SmartspaceSection
+open class SmartspaceSection
 @Inject
 constructor(
     val keyguardClockViewModel: KeyguardClockViewModel,
@@ -45,9 +44,13 @@
     val smartspaceController: LockscreenSmartspaceController,
     val keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
 ) : KeyguardSection() {
-    private var smartspaceView: View? = null
-    private var weatherView: View? = null
-    private var dateView: View? = null
+    var smartspaceView by keyguardSmartspaceViewModel::smartspaceView
+    var weatherView by keyguardSmartspaceViewModel::weatherView
+    var dateView by keyguardSmartspaceViewModel::dateView
+
+    val smartspaceViewId = keyguardSmartspaceViewModel.smartspaceViewId
+    val weatherViewId = keyguardSmartspaceViewModel.weatherId
+    val dateViewId = keyguardSmartspaceViewModel.dateId
 
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!migrateClocksToBlueprint()) {
@@ -67,10 +70,21 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        KeyguardSmartspaceViewBinder.bind(this, constraintLayout, keyguardClockViewModel)
+        if (!migrateClocksToBlueprint()) {
+            return
+        }
+        KeyguardSmartspaceViewBinder.bind(
+            this,
+            constraintLayout,
+            keyguardClockViewModel,
+            keyguardSmartspaceViewModel,
+        )
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
+        if (!migrateClocksToBlueprint()) {
+            return
+        }
         // Generally, weather should be next to dateView
         // smartspace should be below date & weather views
         constraintSet.apply {
@@ -130,7 +144,20 @@
                     }
                 }
             }
-            updateVisibility(constraintSet)
+        }
+        updateVisibility(constraintSet)
+    }
+
+    override fun removeViews(constraintLayout: ConstraintLayout) {
+        if (!migrateClocksToBlueprint()) {
+            return
+        }
+        listOf(smartspaceView, dateView, weatherView).forEach {
+            it?.let {
+                if (it.parent == constraintLayout) {
+                    constraintLayout.removeView(it)
+                }
+            }
         }
     }
 
@@ -158,14 +185,4 @@
             }
         }
     }
-
-    override fun removeViews(constraintLayout: ConstraintLayout) {
-        listOf(smartspaceView, dateView, weatherView).forEach {
-            it?.let {
-                if (it.parent == constraintLayout) {
-                    constraintLayout.removeView(it)
-                }
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt
new file mode 100644
index 0000000..1302bfa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.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.keyguard.ui.view.layout.sections
+
+import android.content.Context
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.SplitShadeStateController
+import javax.inject.Inject
+
+class SplitShadeClockSection
+@Inject
+constructor(
+    clockInteractor: KeyguardClockInteractor,
+    keyguardClockViewModel: KeyguardClockViewModel,
+    smartspaceViewModel: KeyguardSmartspaceViewModel,
+    context: Context,
+    splitShadeStateController: SplitShadeStateController,
+) :
+    ClockSection(
+        clockInteractor,
+        keyguardClockViewModel,
+        smartspaceViewModel,
+        context,
+        splitShadeStateController
+    ) {
+    override fun applyDefaultConstraints(constraints: ConstraintSet) {
+        super.applyDefaultConstraints(constraints)
+        val largeClockEndGuideline =
+            if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID
+            else R.id.split_shade_guideline
+        constraints.apply {
+            connect(
+                R.id.lockscreen_clock_view_large,
+                ConstraintSet.END,
+                largeClockEndGuideline,
+                ConstraintSet.END
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index b0b5c81..0f8e673 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -23,7 +23,6 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
@@ -72,25 +71,9 @@
             return
         }
         constraintSet.apply {
-            val bottomMargin =
-                context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
-
-            if (migrateClocksToBlueprint()) {
-                connect(
-                    R.id.nssl_placeholder,
-                    TOP,
-                    smartspaceViewModel.smartspaceViewId,
-                    TOP,
-                    bottomMargin
-                )
-                setGoneMargin(R.id.nssl_placeholder, TOP, bottomMargin)
-            } else {
-                val splitShadeTopMargin =
-                    context.resources.getDimensionPixelSize(
-                        R.dimen.large_screen_shade_header_height
-                    )
-                connect(R.id.nssl_placeholder, TOP, PARENT_ID, TOP, splitShadeTopMargin)
-            }
+            val splitShadeTopMargin =
+                context.resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
+            connect(R.id.nssl_placeholder, TOP, PARENT_ID, TOP, splitShadeTopMargin)
 
             connect(R.id.nssl_placeholder, START, PARENT_ID, START)
             connect(R.id.nssl_placeholder, END, PARENT_ID, END)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
new file mode 100644
index 0000000..a8e3be7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
@@ -0,0 +1,67 @@
+/*
+ * 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 com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * Breaks down ALTERNATE BOUNCER->AOD transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class AlternateBouncerToAodTransitionViewModel
+@Inject
+constructor(
+    interactor: KeyguardTransitionInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
+            stepFlow = interactor.transition(KeyguardState.ALTERNATE_BOUNCER, KeyguardState.AOD),
+        )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
+            onStep = { 1 - it },
+            onFinish = { 0f },
+        )
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled
+            ->
+            if (udfpsEnrolledAndEnabled) {
+                transitionAnimation.immediatelyTransitionTo(1f)
+            } else {
+                emptyFlow()
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index 023d16ca..5d6b0ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -18,8 +18,12 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TO_GONE_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -33,10 +37,20 @@
 class AlternateBouncerToGoneTransitionViewModel
 @Inject
 constructor(
+    interactor: KeyguardTransitionInteractor,
     bouncerToGoneFlows: BouncerToGoneFlows,
-) {
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = TO_GONE_DURATION,
+            stepFlow = interactor.transition(ALTERNATE_BOUNCER, KeyguardState.GONE),
+        )
 
     /** Scrim alpha values */
     val scrimAlpha: Flow<ScrimAlpha> =
         bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, ALTERNATE_BOUNCER)
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
new file mode 100644
index 0000000..fa4de04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.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.viewmodel
+
+import android.content.Context
+import android.hardware.biometrics.SensorLocationInternal
+import com.android.settingslib.Utils
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/** Models the UI state for the UDFPS icon view in the alternate bouncer view. */
+@ExperimentalCoroutinesApi
+class AlternateBouncerUdfpsIconViewModel
+@Inject
+constructor(
+    val context: Context,
+    configurationInteractor: ConfigurationInteractor,
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    deviceEntryBackgroundViewModel: DeviceEntryBackgroundViewModel,
+    fingerprintPropertyRepository: FingerprintPropertyRepository,
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
+) {
+    private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
+    val iconLocation: Flow<IconLocation> =
+        isSupported.flatMapLatest { supportsUI ->
+            if (supportsUI) {
+                fingerprintPropertyRepository.sensorLocations.map { sensorLocations ->
+                    val sensorLocation =
+                        sensorLocations.getOrDefault("", SensorLocationInternal.DEFAULT)
+                    IconLocation(
+                        left = sensorLocation.sensorLocationX - sensorLocation.sensorRadius,
+                        top = sensorLocation.sensorLocationY - sensorLocation.sensorRadius,
+                        right = sensorLocation.sensorLocationX + sensorLocation.sensorRadius,
+                        bottom = sensorLocation.sensorLocationY + sensorLocation.sensorRadius,
+                    )
+                }
+            } else {
+                emptyFlow()
+            }
+        }
+    val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> =
+        flowOf(DeviceEntryIconView.AccessibilityHintType.ENTER)
+
+    private val fgIconColor: Flow<Int> =
+        configurationInteractor.onAnyConfigurationChange
+            .map { Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary) }
+            .onStart {
+                emit(Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary))
+            }
+    private val fgIconPadding: Flow<Int> = udfpsOverlayInteractor.iconPadding
+    val fgViewModel: Flow<DeviceEntryForegroundViewModel.ForegroundIconViewModel> =
+        combine(
+            fgIconColor,
+            fgIconPadding,
+        ) { color, padding ->
+            DeviceEntryForegroundViewModel.ForegroundIconViewModel(
+                type = DeviceEntryIconView.IconType.FINGERPRINT,
+                useAodVariant = false,
+                tint = color,
+                padding = padding,
+            )
+        }
+
+    val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color
+    val bgAlpha: Flow<Float> = flowOf(1f)
+
+    data class IconLocation(
+        val left: Int,
+        val top: Int,
+        val right: Int,
+        val bottom: Int,
+    ) {
+        val width = right - left
+        val height = bottom - top
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index 3e8bbb3..be9ae1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -19,11 +19,10 @@
 
 import android.content.Context
 import com.android.settingslib.Utils
-import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
@@ -34,7 +33,7 @@
 @Inject
 constructor(
     val context: Context,
-    configurationRepository: ConfigurationRepository, // TODO (b/309655554): create & use interactor
+    configurationInteractor: ConfigurationInteractor,
     lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
     aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
@@ -42,9 +41,10 @@
     occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel,
 ) {
-    private val color: Flow<Int> =
-        configurationRepository.onAnyConfigurationChange
+    val color: Flow<Int> =
+        configurationInteractor.onAnyConfigurationChange
             .map {
                 Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorSurface)
             }
@@ -56,7 +56,7 @@
                     )
                 )
             }
-    private val alpha: Flow<Float> =
+    val alpha: Flow<Float> =
         setOf(
                 lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
                 aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
@@ -65,19 +65,7 @@
                 occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
                 occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                 dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
             )
             .merge()
-
-    val viewModel: Flow<BackgroundViewModel> =
-        combine(color, alpha) { color, alpha ->
-            BackgroundViewModel(
-                alpha = alpha,
-                tint = color,
-            )
-        }
-
-    data class BackgroundViewModel(
-        val alpha: Float,
-        val tint: Int,
-    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
index 9a50d83..fe0b365 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
@@ -19,6 +19,7 @@
 
 import android.content.Context
 import com.android.settingslib.Utils
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -26,7 +27,6 @@
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
 import com.android.systemui.res.R
 import javax.inject.Inject
-import kotlin.math.roundToInt
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -45,6 +45,7 @@
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     transitionInteractor: KeyguardTransitionInteractor,
     deviceEntryIconViewModel: DeviceEntryIconViewModel,
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) {
     private val isShowingAod: Flow<Boolean> =
         transitionInteractor.startedKeyguardState.map { keyguardState ->
@@ -73,11 +74,7 @@
                 isTransitionToAod && isUdfps
             }
             .distinctUntilChanged()
-    private val padding: Flow<Int> =
-        configurationRepository.scaleForResolution.map { scale ->
-            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
-                .roundToInt()
-        }
+    private val padding: Flow<Int> = udfpsOverlayInteractor.iconPadding
 
     val viewModel: Flow<ForegroundIconViewModel> =
         combine(
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
index f95713b..eacaa40 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.sample
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -52,15 +53,12 @@
     transitionInteractor: KeyguardTransitionInteractor,
     val keyguardInteractor: KeyguardInteractor,
     val viewModel: AodToLockscreenTransitionViewModel,
-    val shadeDependentFlows: ShadeDependentFlows,
     private val sceneContainerFlags: SceneContainerFlags,
     private val keyguardViewController: Lazy<KeyguardViewController>,
     private val deviceEntryInteractor: DeviceEntryInteractor,
 ) {
     private val intEvaluator = IntEvaluator()
     private val floatEvaluator = FloatEvaluator()
-    private val toAodFromState: Flow<KeyguardState> =
-        transitionInteractor.transitionStepsToState(KeyguardState.AOD).map { it.from }
     private val showingAlternateBouncer: Flow<Boolean> =
         transitionInteractor.startedKeyguardState.map { keyguardState ->
             keyguardState == KeyguardState.ALTERNATE_BOUNCER
@@ -95,13 +93,31 @@
                 fullyDozingBurnInProgress,
             )
         }
+
+    private val dozeAmount: Flow<Float> =
+        combine(
+            transitionInteractor.startedKeyguardTransitionStep,
+            merge(
+                transitionInteractor.transitionStepsFromState(KeyguardState.AOD).map {
+                    1f - it.value
+                },
+                transitionInteractor.transitionStepsToState(KeyguardState.AOD).map { it.value }
+            ),
+        ) { startedKeyguardTransitionStep, aodTransitionAmount ->
+            if (
+                startedKeyguardTransitionStep.to == KeyguardState.AOD ||
+                    startedKeyguardTransitionStep.from == KeyguardState.AOD
+            ) {
+                aodTransitionAmount
+            } else {
+                // in case a new transition (ie: to occluded) cancels a transition to or from
+                // aod, then we want to make sure the doze amount is still updated to 0
+                0f
+            }
+        }
     // Burn-in offsets that animate based on the transition amount to AOD
     private val animatedBurnInOffsets: Flow<BurnInOffsets> =
-        combine(
-            nonAnimatedBurnInOffsets,
-            transitionInteractor.transitionStepsToState(KeyguardState.AOD)
-        ) { burnInOffsets, transitionStepsToAod ->
-            val dozeAmount = transitionStepsToAod.value
+        combine(nonAnimatedBurnInOffsets, dozeAmount) { burnInOffsets, dozeAmount ->
             BurnInOffsets(
                 intEvaluator.evaluate(dozeAmount, 0, burnInOffsets.x),
                 intEvaluator.evaluate(dozeAmount, 0, burnInOffsets.y),
@@ -120,22 +136,35 @@
     val burnInOffsets: Flow<BurnInOffsets> =
         deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolled ->
             if (udfpsEnrolled) {
-                toAodFromState.flatMapLatest { fromState ->
-                    when (fromState) {
-                        KeyguardState.AOD,
-                        KeyguardState.GONE,
-                        KeyguardState.OCCLUDED,
-                        KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
-                        KeyguardState.OFF,
-                        KeyguardState.DOZING,
-                        KeyguardState.DREAMING,
-                        KeyguardState.PRIMARY_BOUNCER -> nonAnimatedBurnInOffsets
-                        KeyguardState.ALTERNATE_BOUNCER -> animatedBurnInOffsets
-                        KeyguardState.LOCKSCREEN ->
-                            shadeDependentFlows.transitionFlow(
-                                flowWhenShadeIsExpanded = nonAnimatedBurnInOffsets,
-                                flowWhenShadeIsNotExpanded = animatedBurnInOffsets,
-                            )
+                combine(
+                    transitionInteractor.startedKeyguardTransitionStep.sample(
+                        shadeInteractor.isAnyFullyExpanded,
+                        ::Pair
+                    ),
+                    animatedBurnInOffsets,
+                    nonAnimatedBurnInOffsets,
+                ) {
+                    (startedTransitionStep, shadeExpanded),
+                    animatedBurnInOffsets,
+                    nonAnimatedBurnInOffsets ->
+                    if (startedTransitionStep.to == KeyguardState.AOD) {
+                        when (startedTransitionStep.from) {
+                            KeyguardState.ALTERNATE_BOUNCER -> animatedBurnInOffsets
+                            KeyguardState.LOCKSCREEN ->
+                                if (shadeExpanded) {
+                                    nonAnimatedBurnInOffsets
+                                } else {
+                                    animatedBurnInOffsets
+                                }
+                            else -> nonAnimatedBurnInOffsets
+                        }
+                    } else if (startedTransitionStep.from == KeyguardState.AOD) {
+                        when (startedTransitionStep.to) {
+                            KeyguardState.LOCKSCREEN -> animatedBurnInOffsets
+                            else -> BurnInOffsets(x = 0, y = 0, progress = 0f)
+                        }
+                    } else {
+                        BurnInOffsets(x = 0, y = 0, progress = 0f)
                     }
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 3aeff61..528a2ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -27,8 +27,8 @@
 import com.android.systemui.plugins.clocks.ClockController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
 
@@ -79,10 +79,10 @@
                         ?: false
             )
 
-    val clockShouldBeCentered: Flow<Boolean> =
+    val clockShouldBeCentered: StateFlow<Boolean> =
         keyguardInteractor.clockShouldBeCentered.stateIn(
             scope = applicationScope,
             started = SharingStarted.WhileSubscribed(),
-            initialValue = true
+            initialValue = false
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
index 4541458..26e7ee8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.util.Log
+import android.view.View
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
 import javax.inject.Inject
@@ -25,7 +26,7 @@
 @SysUISingleton
 class KeyguardSmartspaceViewModel
 @Inject
-constructor(val context: Context, smartspaceController: LockscreenSmartspaceController) {
+constructor(val context: Context, val smartspaceController: LockscreenSmartspaceController) {
     val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled()
     val isWeatherEnabled: Boolean = smartspaceController.isWeatherEnabled()
     val isDateWeatherDecoupled: Boolean = smartspaceController.isDateWeatherDecoupled()
@@ -38,6 +39,10 @@
     val weatherId: Int
         get() = getId("weather_smartspace_view")
 
+    var smartspaceView: View? = null
+    var weatherView: View? = null
+    var dateView: View? = null
+
     private fun getId(name: String): Int {
         return context.resources.getIdentifier(name, "id", context.packageName).also {
             if (it == 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
new file mode 100644
index 0000000..d57e569
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 com.android.systemui.biometrics.AuthController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class LockscreenContentViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardBlueprintInteractor,
+    private val authController: AuthController,
+) {
+    val isUdfpsVisible: Boolean
+        get() = authController.isUdfpsSupported
+
+    fun blueprintId(scope: CoroutineScope): StateFlow<String> {
+        return interactor.blueprint
+            .map { it.id }
+            .distinctUntilChanged()
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = interactor.getCurrentBlueprint().id,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
deleted file mode 100644
index 6e77e13e..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
+++ /dev/null
@@ -1,47 +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.keyguard.ui.viewmodel
-
-import android.content.Context
-import com.android.systemui.keyguard.domain.interactor.Offsets
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.res.R
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-/** View-model for UDFPS AOD view. */
-@ExperimentalCoroutinesApi
-class UdfpsAodViewModel
-@Inject
-constructor(
-    val interactor: UdfpsKeyguardInteractor,
-    val context: Context,
-) {
-    val alpha: Flow<Float> = interactor.dozeAmount
-    val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets
-    val isVisible: Flow<Boolean> = alpha.map { it != 0f }
-
-    // Padding between the fingerprint icon and its bounding box in pixels.
-    val padding: Flow<Int> =
-        interactor.scaleForResolution.map { scale ->
-            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
-                .roundToInt()
-        }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt
deleted file mode 100644
index d894a11..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt
+++ /dev/null
@@ -1,26 +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.keyguard.ui.viewmodel
-
-import com.android.systemui.biometrics.UdfpsKeyguardAccessibilityDelegate
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-class UdfpsKeyguardInternalViewModel
-@Inject
-constructor(val accessibilityDelegate: UdfpsKeyguardAccessibilityDelegate)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt
deleted file mode 100644
index 098b481..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.graphics.Rect
-import com.android.systemui.biometrics.UdfpsKeyguardView
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.ui.binder.UdfpsKeyguardViewBinder
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-@SysUISingleton
-class UdfpsKeyguardViewModels
-@Inject
-constructor(
-    private val viewModel: UdfpsKeyguardViewModel,
-    private val internalViewModel: UdfpsKeyguardInternalViewModel,
-    private val aodViewModel: UdfpsAodViewModel,
-    private val lockscreenFingerprintViewModel: FingerprintViewModel,
-    private val lockscreenBackgroundViewModel: BackgroundViewModel,
-) {
-
-    fun setSensorBounds(sensorBounds: Rect) {
-        viewModel.sensorBounds = sensorBounds
-    }
-
-    fun bindViews(view: UdfpsKeyguardView) {
-        UdfpsKeyguardViewBinder.bind(
-            view,
-            viewModel,
-            internalViewModel,
-            aodViewModel,
-            lockscreenFingerprintViewModel,
-            lockscreenBackgroundViewModel
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
deleted file mode 100644
index 642904d..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
+++ /dev/null
@@ -1,220 +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.keyguard.ui.viewmodel
-
-import android.content.Context
-import androidx.annotation.ColorInt
-import com.android.settingslib.Utils.getColorAttrDefaultColor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.Offsets
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.res.R
-import com.android.wm.shell.animation.Interpolators
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-
-/** View-model for UDFPS lockscreen views. */
-@ExperimentalCoroutinesApi
-open class UdfpsLockscreenViewModel(
-    context: Context,
-    lockscreenColorResId: Int,
-    alternateBouncerColorResId: Int,
-    transitionInteractor: KeyguardTransitionInteractor,
-    udfpsKeyguardInteractor: UdfpsKeyguardInteractor,
-    keyguardInteractor: KeyguardInteractor,
-) {
-    private val toLockscreen: Flow<TransitionViewModel> =
-        transitionInteractor.anyStateToLockscreenTransition.map {
-            TransitionViewModel(
-                alpha =
-                    if (it.from == KeyguardState.AOD) {
-                        it.value // animate
-                    } else {
-                        1f
-                    },
-                scale = 1f,
-                color = getColorAttrDefaultColor(context, lockscreenColorResId),
-            )
-        }
-
-    private val toAlternateBouncer: Flow<TransitionViewModel> =
-        keyguardInteractor.statusBarState.flatMapLatest { statusBarState ->
-            transitionInteractor.transitionStepsToState(KeyguardState.ALTERNATE_BOUNCER).map {
-                TransitionViewModel(
-                    alpha = 1f,
-                    scale =
-                        if (visibleInKeyguardState(it.from, statusBarState)) {
-                            1f
-                        } else {
-                            Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it.value)
-                        },
-                    color = getColorAttrDefaultColor(context, alternateBouncerColorResId),
-                )
-            }
-        }
-
-    private val fadeOut: Flow<TransitionViewModel> =
-        keyguardInteractor.statusBarState.flatMapLatest { statusBarState ->
-            merge(
-                    transitionInteractor.anyStateToGoneTransition,
-                    transitionInteractor.anyStateToAodTransition,
-                    transitionInteractor.anyStateToOccludedTransition,
-                    transitionInteractor.anyStateToPrimaryBouncerTransition,
-                    transitionInteractor.anyStateToDreamingTransition,
-                )
-                .map {
-                    TransitionViewModel(
-                        alpha =
-                            if (visibleInKeyguardState(it.from, statusBarState)) {
-                                1f - it.value
-                            } else {
-                                0f
-                            },
-                        scale = 1f,
-                        color =
-                            if (it.from == KeyguardState.ALTERNATE_BOUNCER) {
-                                getColorAttrDefaultColor(context, alternateBouncerColorResId)
-                            } else {
-                                getColorAttrDefaultColor(context, lockscreenColorResId)
-                            },
-                    )
-                }
-        }
-
-    private fun visibleInKeyguardState(
-        state: KeyguardState,
-        statusBarState: StatusBarState
-    ): Boolean {
-        return when (state) {
-            KeyguardState.OFF,
-            KeyguardState.DOZING,
-            KeyguardState.DREAMING,
-            KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
-            KeyguardState.AOD,
-            KeyguardState.PRIMARY_BOUNCER,
-            KeyguardState.GONE,
-            KeyguardState.OCCLUDED -> false
-            KeyguardState.LOCKSCREEN -> statusBarState == StatusBarState.KEYGUARD
-            KeyguardState.ALTERNATE_BOUNCER -> true
-        }
-    }
-
-    private val keyguardStateTransition =
-        merge(
-            toAlternateBouncer,
-            toLockscreen,
-            fadeOut,
-        )
-
-    private val dialogHideAffordancesAlphaMultiplier: Flow<Float> =
-        udfpsKeyguardInteractor.dialogHideAffordancesRequest.map { hideAffordances ->
-            if (hideAffordances) {
-                0f
-            } else {
-                1f
-            }
-        }
-
-    private val alphaMultiplier: Flow<Float> =
-        combine(
-            transitionInteractor.startedKeyguardState,
-            dialogHideAffordancesAlphaMultiplier,
-            udfpsKeyguardInteractor.shadeExpansion,
-            udfpsKeyguardInteractor.qsProgress,
-        ) { startedKeyguardState, dialogHideAffordancesAlphaMultiplier, shadeExpansion, qsProgress
-            ->
-            if (startedKeyguardState == KeyguardState.ALTERNATE_BOUNCER) {
-                1f
-            } else {
-                dialogHideAffordancesAlphaMultiplier * (1f - shadeExpansion) * (1f - qsProgress)
-            }
-        }
-
-    val transition: Flow<TransitionViewModel> =
-        combine(
-            alphaMultiplier,
-            keyguardStateTransition,
-        ) { alphaMultiplier, keyguardStateTransition ->
-            TransitionViewModel(
-                alpha = keyguardStateTransition.alpha * alphaMultiplier,
-                scale = keyguardStateTransition.scale,
-                color = keyguardStateTransition.color,
-            )
-        }
-    val visible: Flow<Boolean> = transition.map { it.alpha != 0f }
-}
-
-@ExperimentalCoroutinesApi
-class FingerprintViewModel
-@Inject
-constructor(
-    val context: Context,
-    transitionInteractor: KeyguardTransitionInteractor,
-    interactor: UdfpsKeyguardInteractor,
-    keyguardInteractor: KeyguardInteractor,
-) :
-    UdfpsLockscreenViewModel(
-        context,
-        android.R.attr.textColorPrimary,
-        com.android.internal.R.attr.materialColorOnPrimaryFixed,
-        transitionInteractor,
-        interactor,
-        keyguardInteractor,
-    ) {
-    val dozeAmount: Flow<Float> = interactor.dozeAmount
-    val burnInOffsets: Flow<Offsets> = interactor.burnInOffsets
-
-    // Padding between the fingerprint icon and its bounding box in pixels.
-    val padding: Flow<Int> =
-        interactor.scaleForResolution.map { scale ->
-            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
-                .roundToInt()
-        }
-}
-
-@ExperimentalCoroutinesApi
-class BackgroundViewModel
-@Inject
-constructor(
-    val context: Context,
-    transitionInteractor: KeyguardTransitionInteractor,
-    interactor: UdfpsKeyguardInteractor,
-    keyguardInteractor: KeyguardInteractor,
-) :
-    UdfpsLockscreenViewModel(
-        context,
-        com.android.internal.R.attr.colorSurface,
-        com.android.internal.R.attr.materialColorPrimaryFixed,
-        transitionInteractor,
-        interactor,
-        keyguardInteractor,
-    )
-
-data class TransitionViewModel(
-    val alpha: Float,
-    val scale: Float,
-    @ColorInt val color: Int,
-)
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 d8bb3e6..0d5ba64 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -26,6 +26,7 @@
 import com.android.systemui.log.echo.LogcatEchoTrackerProd;
 import com.android.systemui.log.table.TableLogBuffer;
 import com.android.systemui.log.table.TableLogBufferFactory;
+import com.android.systemui.plugins.clocks.ClockMessageBuffers;
 import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository;
 import com.android.systemui.qs.pipeline.shared.TileSpec;
@@ -417,6 +418,18 @@
     }
 
     /**
+     * Provides a {@link ClockMessageBuffers} which contains the keyguard clock message buffers.
+     */
+    @Provides
+    public static ClockMessageBuffers provideKeyguardClockMessageBuffers(
+            @KeyguardClockLog LogBuffer infraClockLog,
+            @KeyguardSmallClockLog LogBuffer smallClockLog,
+            @KeyguardLargeClockLog LogBuffer largeClockLog
+    ) {
+        return new ClockMessageBuffers(infraClockLog, smallClockLog, largeClockLog);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index 724241d..185a783 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.pipeline
 
 import android.content.Context
+import android.content.pm.UserInfo
 import android.os.SystemProperties
 import android.util.Log
 import com.android.internal.annotations.KeepForWeakReference
@@ -88,7 +89,11 @@
     private val userTrackerCallback =
         object : UserTracker.Callback {
             override fun onUserChanged(newUser: Int, userContext: Context) {
-                handleUserSwitched(newUser)
+                handleUserSwitched()
+            }
+
+            override fun onProfilesChanged(profiles: List<UserInfo>) {
+                handleProfileChanged()
             }
         }
 
@@ -109,7 +114,10 @@
         }
         allEntries.put(key, data)
 
-        if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
+        if (
+            !lockscreenUserManager.isCurrentProfile(data.userId) ||
+                !lockscreenUserManager.isProfileAvailable(data.userId)
+        ) {
             return
         }
 
@@ -231,7 +239,20 @@
     }
 
     @VisibleForTesting
-    internal fun handleUserSwitched(id: Int) {
+    internal fun handleProfileChanged() {
+        // TODO(b/317221348) re-add media removed when profile is available.
+        allEntries.forEach { (key, data) ->
+            if (!lockscreenUserManager.isProfileAvailable(data.userId)) {
+                // Only remove media when the profile is unavailable.
+                if (DEBUG) Log.d(TAG, "Removing $key after profile change")
+                userEntries.remove(key, data)
+                listeners.forEach { listener -> listener.onMediaDataRemoved(key) }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun handleUserSwitched() {
         // If the user changes, remove all current MediaData objects and inform listeners
         val listenersCopy = listeners
         val keyCopy = userEntries.keys.toMutableList()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index a99c51c2..be93936 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -405,12 +405,15 @@
         }
         widgetGroupIds.forEach { id ->
             squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha =
-                    calculateAlpha(
-                        squishFraction,
-                        startPosition / nonsquishedHeight,
-                        endPosition / nonsquishedHeight
-                    )
+                // Don't modify alpha for elements that should be invisible (e.g. disabled seekbar)
+                if (state.alpha != 0f) {
+                    state.alpha =
+                        calculateAlpha(
+                            squishFraction,
+                            startPosition / nonsquishedHeight,
+                            endPosition / nonsquishedHeight
+                        )
+                }
             }
         }
         return groupTop // used for the widget group above this group
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt
index 898298c..77279f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaInSceneContainerFlag.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.util
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the media_in_scene_container flag state. */
@@ -25,6 +26,10 @@
     /** The aconfig flag name */
     const val FLAG_NAME = Flags.FLAG_MEDIA_IN_SCENE_CONTAINER
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is the flag enabled? */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index e827a1e..3e6d46c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -25,12 +25,12 @@
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
-import com.android.systemui.res.R
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
 import com.android.systemui.temporarydisplay.ViewPriority
@@ -162,7 +162,7 @@
         logger: MediaTttSenderLogger,
         instanceId: InstanceId,
     ): ChipbarInfo {
-        val packageName = checkNotNull(routeInfo.clientPackageName)
+        val packageName = routeInfo.clientPackageName
         val otherDeviceName =
             if (routeInfo.name.isBlank()) {
                 context.getString(R.string.media_ttt_default_device_type)
@@ -171,7 +171,7 @@
             }
         val icon =
             MediaTttUtils.getIconInfoFromPackageName(context, packageName, isReceiver = false) {
-                logger.logPackageNotFound(packageName)
+                packageName?.let { logger.logPackageNotFound(it) }
             }
 
         val timeout =
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt
index 3c50127..2408af1 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt
@@ -17,23 +17,22 @@
 package com.android.systemui.mediaprojection.taskswitcher
 
 import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.pssTaskSwitcher
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.mediaprojection.taskswitcher.ui.TaskSwitcherNotificationCoordinator
+import dagger.Lazy
 import javax.inject.Inject
 
 @SysUISingleton
 class MediaProjectionTaskSwitcherCoreStartable
 @Inject
 constructor(
-    private val notificationCoordinator: TaskSwitcherNotificationCoordinator,
-    private val featureFlags: FeatureFlags,
+    private val notificationCoordinatorLazy: Lazy<TaskSwitcherNotificationCoordinator>,
 ) : CoreStartable {
 
     override fun start() {
-        if (featureFlags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)) {
-            notificationCoordinator.start()
+        if (pssTaskSwitcher()) {
+            notificationCoordinatorLazy.get().start()
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 0e7e69b..3aa9daa 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -37,7 +37,7 @@
 import android.provider.Settings
 import android.widget.Toast
 import androidx.annotation.VisibleForTesting
-import com.android.app.tracing.TraceUtils.Companion.launch
+import com.android.app.tracing.coroutines.launch
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -263,11 +263,7 @@
      * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the
      * Widget Picker to all users.
      */
-    // TODO(b/316332684)
-    @Suppress("UNREACHABLE_CODE")
     fun setNoteTaskShortcutEnabled(value: Boolean, user: UserHandle) {
-        return // shortcut should not be enabled until additional features are implemented.
-
         if (!userManager.isUserUnlocked(user)) {
             debugLog { "setNoteTaskShortcutEnabled call but user locked: user=$user" }
             return
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 1534653..958ace35 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -48,12 +48,13 @@
 import com.android.settingslib.fuelgauge.Estimate;
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.CoreStartable;
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -62,7 +63,10 @@
 import javax.inject.Inject;
 
 @SysUISingleton
-public class PowerUI implements CoreStartable, CommandQueue.Callbacks {
+public class PowerUI implements
+        CoreStartable,
+        ConfigurationController.ConfigurationListener,
+        CommandQueue.Callbacks {
 
     static final String TAG = "PowerUI";
     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -223,7 +227,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         final int mask = ActivityInfo.CONFIG_MCC | ActivityInfo.CONFIG_MNC;
 
         // Safe to modify mLastConfiguration here as it's only updated by the main thread (here).
diff --git a/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java b/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java
index 7184fa0..8dd0ea0 100644
--- a/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java
+++ b/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java
@@ -16,14 +16,19 @@
 
 package com.android.systemui.power.dagger;
 
+import com.android.systemui.CoreStartable;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.power.EnhancedEstimatesImpl;
 import com.android.systemui.power.PowerNotificationWarnings;
 import com.android.systemui.power.PowerUI;
 import com.android.systemui.power.data.repository.PowerRepositoryModule;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import dagger.Binds;
 import dagger.Module;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+import dagger.multibindings.IntoSet;
 
 
 /** Dagger Module for code in the power package. */
@@ -33,6 +38,17 @@
         }
 )
 public interface PowerModule {
+    /** Starts PowerUI.  */
+    @Binds
+    @IntoMap
+    @ClassKey(PowerUI.class)
+    CoreStartable bindPowerUIStartable(PowerUI impl);
+
+    /** Listen to config changes for PowerUI.  */
+    @Binds
+    @IntoSet
+    ConfigurationController.ConfigurationListener bindPowerUIConfigChanges(PowerUI impl);
+
     /** */
     @Binds
     EnhancedEstimates bindEnhancedEstimates(EnhancedEstimatesImpl enhancedEstimates);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
index 1ab64b7..ba3357c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
@@ -17,12 +17,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
-import android.os.Build;
 import android.provider.Settings;
 
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.util.leak.GarbageMonitor;
+import com.android.systemui.res.R;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -44,10 +42,6 @@
         final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
 
         tiles.addAll(Arrays.asList(defaultTileList.split(",")));
-        if (Build.IS_DEBUGGABLE
-                && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) {
-            tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
         return tiles;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 2af7ae0..47b0624 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -23,7 +23,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
 import android.service.quicksettings.TileService;
@@ -33,7 +32,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.qs.QSTile;
@@ -42,8 +40,8 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.util.leak.GarbageMonitor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -114,9 +112,6 @@
                 possibleTiles.add(spec);
             }
         }
-        if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) {
-            possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
 
         final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
         possibleTiles.remove("cell");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java b/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java
index 2345667..83b6f0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java
@@ -19,16 +19,21 @@
 import android.service.quicksettings.IQSTileService;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 
 public class QSTileServiceWrapper {
     private static final String TAG = "IQSTileServiceWrapper";
 
+    @NonNull
     private final IQSTileService mService;
 
-    public QSTileServiceWrapper(IQSTileService service) {
+    public QSTileServiceWrapper(@NonNull IQSTileService service) {
         mService = service;
     }
 
+    // This can never be null, as it's the constructor parameter and it's final
+    @NonNull
     public IBinder asBinder() {
         return mService.asBinder();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 789a1e4..880289e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -40,6 +40,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
@@ -54,8 +55,10 @@
 
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
 
 /**
  * Manages the lifecycle of a TileService.
@@ -66,7 +69,8 @@
  */
 public class TileLifecycleManager extends BroadcastReceiver implements
         IQSTileService, ServiceConnection, IBinder.DeathRecipient {
-    public static final boolean DEBUG = false;
+
+    private final boolean mDebug = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final String TAG = "TileLifecycleManager";
 
@@ -100,8 +104,8 @@
     private final ActivityManager mActivityManager;
 
     private Set<Integer> mQueuedMessages = new ArraySet<>();
-    @Nullable
-    private QSTileServiceWrapper mWrapper;
+    @NonNull
+    private volatile Optional<QSTileServiceWrapper> mOptionalWrapper = Optional.empty();
     private boolean mListening;
     private IBinder mClickBinder;
 
@@ -132,7 +136,7 @@
         mPackageManagerAdapter = packageManagerAdapter;
         mBroadcastDispatcher = broadcastDispatcher;
         mActivityManager = activityManager;
-        if (DEBUG) Log.d(TAG, "Creating " + mIntent + " " + mUser);
+        if (mDebug) Log.d(TAG, "Creating " + mIntent + " " + mUser);
     }
 
     /** Injectable factory for TileLifecycleManager. */
@@ -215,11 +219,13 @@
             if (!checkComponentState()) {
                 return;
             }
-            if (DEBUG) Log.d(TAG, "Binding service " + mIntent + " " + mUser);
+            if (mDebug) Log.d(TAG, "Binding service " + mIntent + " " + mUser);
             mBindTryCount++;
             try {
-                mIsBound.set(bindServices());
+                // Only try a new binding if we are not currently bound.
+                mIsBound.compareAndSet(false, bindServices());
                 if (!mIsBound.get()) {
+                    Log.d(TAG, "Failed to bind to service");
                     mContext.unbindService(this);
                 }
             } catch (SecurityException e) {
@@ -227,19 +233,7 @@
                 mIsBound.set(false);
             }
         } else {
-            if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent + " " + mUser);
-            // Give it another chance next time it needs to be bound, out of kindness.
-            mBindTryCount = 0;
-            freeWrapper();
-            if (mIsBound.get()) {
-                try {
-                    mContext.unbindService(this);
-                } catch (Exception e) {
-                    Log.e(TAG, "Failed to unbind service "
-                            + mIntent.getComponent().flattenToShortString(), e);
-                }
-                mIsBound.set(false);
-            }
+            unbindService();
         }
     }
 
@@ -264,9 +258,26 @@
                 mUser);
     }
 
+    @WorkerThread
+    private void unbindService() {
+        if (mDebug) Log.d(TAG, "Unbinding service " + mIntent + " " + mUser);
+        // Give it another chance next time it needs to be bound, out of kindness.
+        mBindTryCount = 0;
+        freeWrapper();
+        if (mIsBound.get()) {
+            try {
+                mContext.unbindService(this);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to unbind service "
+                        + mIntent.getComponent().flattenToShortString(), e);
+            }
+            mIsBound.set(false);
+        }
+    }
+
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
-        if (DEBUG) Log.d(TAG, "onServiceConnected " + name);
+        if (mDebug) Log.d(TAG, "onServiceConnected " + name);
         // Got a connection, set the binding count to 0.
         mBindTryCount = 0;
         final QSTileServiceWrapper wrapper = new QSTileServiceWrapper(Stub.asInterface(service));
@@ -274,7 +285,7 @@
             service.linkToDeath(this, 0);
         } catch (RemoteException e) {
         }
-        mWrapper = wrapper;
+        mOptionalWrapper = Optional.of(wrapper);
         handlePendingMessages();
     }
 
@@ -284,11 +295,17 @@
     }
 
     @Override
-    public void onServiceDisconnected(ComponentName name) {
-        if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
+    public void onBindingDied(ComponentName name) {
+        if (mDebug) Log.d(TAG, "onBindingDied " + name);
         handleDeath();
     }
 
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        if (mDebug) Log.d(TAG, "onServiceDisconnected " + name);
+        freeWrapper();
+    }
+
     private void handlePendingMessages() {
         // This ordering is laid out manually to make sure we preserve the TileService
         // lifecycle.
@@ -298,35 +315,36 @@
             mQueuedMessages.clear();
         }
         if (queue.contains(MSG_ON_ADDED)) {
-            if (DEBUG) Log.d(TAG, "Handling pending onAdded");
+            if (mDebug) Log.d(TAG, "Handling pending onAdded " + getComponent());
             onTileAdded();
         }
         if (mListening) {
-            if (DEBUG) Log.d(TAG, "Handling pending onStartListening");
+            if (mDebug) Log.d(TAG, "Handling pending onStartListening " + getComponent());
             onStartListening();
         }
         if (queue.contains(MSG_ON_CLICK)) {
-            if (DEBUG) Log.d(TAG, "Handling pending onClick");
+            if (mDebug) Log.d(TAG, "Handling pending onClick " + getComponent());
             if (!mListening) {
-                Log.w(TAG, "Managed to get click on non-listening state...");
+                Log.w(TAG, "Managed to get click on non-listening state... " + getComponent());
                 // Skipping click since lost click privileges.
             } else {
                 onClick(mClickBinder);
             }
         }
         if (queue.contains(MSG_ON_UNLOCK_COMPLETE)) {
-            if (DEBUG) Log.d(TAG, "Handling pending onUnlockComplete");
+            if (mDebug) Log.d(TAG, "Handling pending onUnlockComplete " + getComponent());
             if (!mListening) {
-                Log.w(TAG, "Managed to get unlock on non-listening state...");
+                Log.w(TAG,
+                        "Managed to get unlock on non-listening state... " + getComponent());
                 // Skipping unlock since lost click privileges.
             } else {
                 onUnlockComplete();
             }
         }
         if (queue.contains(MSG_ON_REMOVED)) {
-            if (DEBUG) Log.d(TAG, "Handling pending onRemoved");
+            if (mDebug) Log.d(TAG, "Handling pending onRemoved " + getComponent());
             if (mListening) {
-                Log.w(TAG, "Managed to get remove in listening state...");
+                Log.w(TAG, "Managed to get remove in listening state... " + getComponent());
                 onStopListening();
             }
             onTileRemoved();
@@ -340,28 +358,48 @@
     }
 
     public void handleDestroy() {
-        if (DEBUG) Log.d(TAG, "handleDestroy");
+        if (mDebug) Log.d(TAG, "handleDestroy");
         if (mPackageReceiverRegistered.get() || mUserReceiverRegistered.get()) {
             stopPackageListening();
         }
         mChangeListener = null;
     }
 
+    /**
+     * Handles a dead binder.
+     *
+     * It means that we need to clean up the binding (calling unbindService). After that, if we
+     * are supposed to be bound, we will try to bind after some amount of time.
+     */
     private void handleDeath() {
-        if (mWrapper == null) return;
-        freeWrapper();
-        // Clearly not bound anymore
-        mIsBound.set(false);
-        if (!mBound.get()) return;
-        if (DEBUG) Log.d(TAG, "handleDeath");
-        if (checkComponentState()) {
-            if (isDeathRebindScheduled.compareAndSet(false, true)) {
-                mExecutor.executeDelayed(() -> {
-                    setBindService(true);
-                    isDeathRebindScheduled.set(false);
-                }, getRebindDelay());
-            }
+        if (!mIsBound.get()) {
+            // If we are already not bound, don't do anything else.
+            return;
         }
+        mExecutor.execute(() -> {
+            if (!mIsBound.get()) {
+                // If we are already not bound, don't do anything else.
+                return;
+            }
+            // Clearly we shouldn't be bound anymore
+            if (mDebug) Log.d(TAG, "handleDeath " + getComponent());
+            // Binder died, make sure that we unbind. However, we don't want to call setBindService
+            // as we still may want to rebind.
+            unbindService();
+            // If mBound is true (meaning that we should be bound), then reschedule binding for
+            // later.
+            if (mBound.get() && checkComponentState()) {
+                if (isDeathRebindScheduled.compareAndSet(false, true)) {
+                    mExecutor.executeDelayed(() -> {
+                        // Only rebind if we are supposed to, but remove the scheduling anyway.
+                        if (mBound.get()) {
+                            setBindService(true);
+                        }
+                        isDeathRebindScheduled.set(false);
+                    }, getRebindDelay());
+                }
+            }
+        });
     }
 
     /**
@@ -379,7 +417,7 @@
         } else {
             delay = mBindRetryDelay;
         }
-        Log.i(TAG, "Rebinding with a delay=" + delay);
+        if (mDebug) Log.i(TAG, "Rebinding with a delay=" + delay + " - " + getComponent());
         return delay;
     }
 
@@ -392,7 +430,7 @@
     }
 
     private void startPackageListening() {
-        if (DEBUG) Log.d(TAG, "startPackageListening");
+        if (mDebug) Log.d(TAG, "startPackageListening " + getComponent());
         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         filter.addDataScheme("package");
@@ -402,7 +440,7 @@
                     this, mUser, filter, null, mHandler, Context.RECEIVER_EXPORTED);
         } catch (Exception ex) {
             mPackageReceiverRegistered.set(false);
-            Log.e(TAG, "Could not register package receiver", ex);
+            Log.e(TAG, "Could not register package receiver " + getComponent(), ex);
         }
         filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
         try {
@@ -410,12 +448,12 @@
             mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler, mUser);
         } catch (Exception ex) {
             mUserReceiverRegistered.set(false);
-            Log.e(TAG, "Could not register unlock receiver", ex);
+            Log.e(TAG, "Could not register unlock receiver " + getComponent(), ex);
         }
     }
 
     private void stopPackageListening() {
-        if (DEBUG) Log.d(TAG, "stopPackageListening");
+        if (mDebug) Log.d(TAG, "stopPackageListening " + getComponent());
         if (mUserReceiverRegistered.compareAndSet(true, false)) {
             mBroadcastDispatcher.unregisterReceiver(this);
         }
@@ -430,7 +468,7 @@
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        if (DEBUG) Log.d(TAG, "onReceive: " + intent);
+        if (mDebug) Log.d(TAG, "onReceive: " + intent);
         if (!Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
             Uri data = intent.getData();
             String pkgName = data.getEncodedSchemeSpecificPart();
@@ -446,7 +484,7 @@
             if (mBound.get()) {
                 // Trying to bind again will check the state of the package before bothering to
                 // bind.
-                if (DEBUG) Log.d(TAG, "Trying to rebind");
+                if (mDebug) Log.d(TAG, "Trying to rebind " + getComponent());
                 setBindService(true);
             }
 
@@ -458,7 +496,9 @@
         try {
             ServiceInfo si = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
                     0, mUser.getIdentifier());
-            if (DEBUG && si == null) Log.d(TAG, "Can't find component " + mIntent.getComponent());
+            if (mDebug && si == null) {
+                Log.d(TAG, "Can't find component " + mIntent.getComponent());
+            }
             return si != null;
         } catch (RemoteException e) {
             // Shouldn't happen.
@@ -472,7 +512,7 @@
             mPackageManagerAdapter.getPackageInfoAsUser(packageName, 0, mUser.getIdentifier());
             return true;
         } catch (PackageManager.NameNotFoundException e) {
-            if (DEBUG) {
+            if (mDebug) {
                 Log.d(TAG, "Package not available: " + packageName, e);
             } else {
                 Log.d(TAG, "Package not available: " + packageName);
@@ -489,8 +529,8 @@
 
     @Override
     public void onTileAdded() {
-        if (DEBUG) Log.d(TAG, "onTileAdded");
-        if (mWrapper == null || !mWrapper.onTileAdded()) {
+        if (mDebug) Log.d(TAG, "onTileAdded " + getComponent());
+        if (isNullOrFailedAction(mOptionalWrapper, QSTileServiceWrapper::onTileAdded)) {
             queueMessage(MSG_ON_ADDED);
             handleDeath();
         }
@@ -498,8 +538,8 @@
 
     @Override
     public void onTileRemoved() {
-        if (DEBUG) Log.d(TAG, "onTileRemoved");
-        if (mWrapper == null || !mWrapper.onTileRemoved()) {
+        if (mDebug) Log.d(TAG, "onTileRemoved " + getComponent());
+        if (isNullOrFailedAction(mOptionalWrapper, QSTileServiceWrapper::onTileRemoved)) {
             queueMessage(MSG_ON_REMOVED);
             handleDeath();
         }
@@ -507,26 +547,26 @@
 
     @Override
     public void onStartListening() {
-        if (DEBUG) Log.d(TAG, "onStartListening");
+        if (mDebug) Log.d(TAG, "onStartListening " + getComponent());
         mListening = true;
-        if (mWrapper != null && !mWrapper.onStartListening()) {
+        if (isNotNullAndFailedAction(mOptionalWrapper, QSTileServiceWrapper::onStartListening)) {
             handleDeath();
         }
     }
 
     @Override
     public void onStopListening() {
-        if (DEBUG) Log.d(TAG, "onStopListening");
+        if (mDebug) Log.d(TAG, "onStopListening " + getComponent());
         mListening = false;
-        if (mWrapper != null && !mWrapper.onStopListening()) {
+        if (isNotNullAndFailedAction(mOptionalWrapper, QSTileServiceWrapper::onStopListening)) {
             handleDeath();
         }
     }
 
     @Override
     public void onClick(IBinder iBinder) {
-        if (DEBUG) Log.d(TAG, "onClick " + iBinder + " " + mUser);
-        if (mWrapper == null || !mWrapper.onClick(iBinder)) {
+        if (mDebug) Log.d(TAG, "onClick " + iBinder + " " + getComponent() + " " + mUser);
+        if (isNullOrFailedAction(mOptionalWrapper, (wrapper) -> wrapper.onClick(iBinder))) {
             mClickBinder = iBinder;
             queueMessage(MSG_ON_CLICK);
             handleDeath();
@@ -535,8 +575,8 @@
 
     @Override
     public void onUnlockComplete() {
-        if (DEBUG) Log.d(TAG, "onUnlockComplete");
-        if (mWrapper == null || !mWrapper.onUnlockComplete()) {
+        if (mDebug) Log.d(TAG, "onUnlockComplete " + getComponent());
+        if (isNullOrFailedAction(mOptionalWrapper, QSTileServiceWrapper::onUnlockComplete)) {
             queueMessage(MSG_ON_UNLOCK_COMPLETE);
             handleDeath();
         }
@@ -545,12 +585,12 @@
     @Nullable
     @Override
     public IBinder asBinder() {
-        return mWrapper != null ? mWrapper.asBinder() : null;
+        return mOptionalWrapper.map(QSTileServiceWrapper::asBinder).orElse(null);
     }
 
     @Override
     public void binderDied() {
-        if (DEBUG) Log.d(TAG, "binderDeath");
+        if (mDebug) Log.d(TAG, "binderDeath " + getComponent());
         handleDeath();
     }
 
@@ -559,18 +599,42 @@
     }
 
     private void freeWrapper() {
-        if (mWrapper != null) {
+        if (mOptionalWrapper.isPresent()) {
             try {
-                mWrapper.asBinder().unlinkToDeath(this, 0);
+                mOptionalWrapper.ifPresent(
+                        (wrapper) -> wrapper.asBinder().unlinkToDeath(this, 0)
+                );
             } catch (NoSuchElementException e) {
                 Log.w(TAG, "Trying to unlink not linked recipient for component"
                         + mIntent.getComponent().flattenToShortString());
             }
-            mWrapper = null;
+            mOptionalWrapper = Optional.empty();
         }
     }
 
     public interface TileChangeListener {
         void onTileChanged(ComponentName tile);
     }
+
+    /**
+     * Returns true if the Optional is empty OR performing the action on the content of the Optional
+     * (when not empty) fails.
+     */
+    private static boolean isNullOrFailedAction(
+            Optional<QSTileServiceWrapper> optionalWrapper,
+            Predicate<QSTileServiceWrapper> action
+    ) {
+        return !optionalWrapper.map(action::test).orElse(false);
+    }
+
+    /**
+     * Returns true if the Optional is not empty AND performing the action on the content of
+     * the Optional fails.
+     */
+    private static boolean isNotNullAndFailedAction(
+            Optional<QSTileServiceWrapper> optionalWrapper,
+            Predicate<QSTileServiceWrapper> action
+    ) {
+        return  !optionalWrapper.map(action::test).orElse(true);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 17e6375..bdcbac0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.qs.tileimpl;
 
-import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -25,15 +24,14 @@
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.external.CustomTile;
-import com.android.systemui.util.leak.GarbageMonitor;
-
-import dagger.Lazy;
 
 import java.util.Map;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import dagger.Lazy;
+
 /**
  * A factory that creates Quick Settings tiles based on a tileSpec
  *
@@ -79,9 +77,7 @@
     @Nullable
     protected QSTileImpl createTileInternal(String tileSpec) {
         // Stock tiles.
-        if (mTileMap.containsKey(tileSpec)
-                // We should not return a Garbage Monitory Tile if the build is not Debuggable
-                && (!tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC) || Build.IS_DEBUGGABLE)) {
+        if (mTileMap.containsKey(tileSpec)) {
             return mTileMap.get(tileSpec).get();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index 66da8bd..216d716 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -42,9 +42,7 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.recordissue.RecordIssueDialogDelegate
 import com.android.systemui.res.R
-import com.android.systemui.settings.UserContextProvider
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
-import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
 
@@ -63,8 +61,7 @@
     private val keyguardDismissUtil: KeyguardDismissUtil,
     private val keyguardStateController: KeyguardStateController,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val sysuiDialogFactory: SystemUIDialog.Factory,
-    private val userContextProvider: UserContextProvider,
+    private val delegateFactory: RecordIssueDialogDelegate.Factory,
 ) :
     QSTileImpl<QSTile.BooleanState>(
         host,
@@ -102,7 +99,8 @@
 
     private fun showPrompt(view: View?) {
         val dialog: AlertDialog =
-            RecordIssueDialogDelegate(sysuiDialogFactory, userContextProvider) {
+            delegateFactory
+                .create {
                     isRecording = true
                     refreshState()
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
index ca5302e..c932cee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
@@ -16,11 +16,15 @@
 
 package com.android.systemui.qs.tiles.impl.custom.data.repository
 
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
 import android.graphics.drawable.Icon
 import android.os.UserHandle
 import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.external.CustomTileStatePersister
+import com.android.systemui.qs.external.PackageManagerAdapter
 import com.android.systemui.qs.external.TileServiceKey
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.commons.copy
@@ -63,6 +67,12 @@
      */
     fun getTile(user: UserHandle): Tile?
 
+    /** @see [com.android.systemui.qs.external.TileLifecycleManager.isActiveTile] */
+    suspend fun isTileActive(): Boolean
+
+    /** @see [com.android.systemui.qs.external.TileLifecycleManager.isToggleableTile] */
+    suspend fun isTileToggleable(): Boolean
+
     /**
      * Updates tile with the non-null values from [newTile]. Overwrites the current cache when
      * [user] differs from the cached one. [isPersistable] tile will be persisted to be possibly
@@ -92,6 +102,7 @@
 constructor(
     private val tileSpec: TileSpec.CustomTileSpec,
     private val customTileStatePersister: CustomTileStatePersister,
+    private val packageManagerAdapter: PackageManagerAdapter,
     @Background private val backgroundContext: CoroutineContext,
 ) : CustomTileRepository {
 
@@ -149,6 +160,34 @@
         }
     }
 
+    override suspend fun isTileActive(): Boolean =
+        withContext(backgroundContext) {
+            try {
+                val info: ServiceInfo =
+                    packageManagerAdapter.getServiceInfo(
+                        tileSpec.componentName,
+                        META_DATA_QUERY_FLAGS
+                    )
+                info.metaData?.getBoolean(TileService.META_DATA_ACTIVE_TILE, false) == true
+            } catch (e: PackageManager.NameNotFoundException) {
+                false
+            }
+        }
+
+    override suspend fun isTileToggleable(): Boolean =
+        withContext(backgroundContext) {
+            try {
+                val info: ServiceInfo =
+                    packageManagerAdapter.getServiceInfo(
+                        tileSpec.componentName,
+                        META_DATA_QUERY_FLAGS
+                    )
+                info.metaData?.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false) == true
+            } catch (e: PackageManager.NameNotFoundException) {
+                false
+            }
+        }
+
     private suspend fun updateTile(
         user: UserHandle,
         isPersistable: Boolean,
@@ -193,4 +232,12 @@
     private fun UserHandle.getKey() = TileServiceKey(tileSpec.componentName, this.identifier)
 
     private data class TileWithUser(val user: UserHandle, val tile: Tile)
+
+    private companion object {
+        const val META_DATA_QUERY_FLAGS =
+            (PackageManager.GET_META_DATA or
+                PackageManager.MATCH_UNINSTALLED_PACKAGES or
+                PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+                PackageManager.MATCH_DIRECT_BOOT_AWARE)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
index 351bba5..10b012d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
@@ -19,10 +19,9 @@
 import android.os.UserHandle
 import android.service.quicksettings.Tile
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.qs.external.TileServiceManager
 import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
 import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository
-import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundScope
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
@@ -35,15 +34,13 @@
 import kotlinx.coroutines.flow.onEach
 
 /** Manages updates of the [Tile] assigned for the current custom tile. */
-@CustomTileBoundScope
+@QSTileScope
 class CustomTileInteractor
 @Inject
 constructor(
-    private val user: UserHandle,
     private val defaultsRepository: CustomTileDefaultsRepository,
     private val customTileRepository: CustomTileRepository,
-    private val tileServiceManager: TileServiceManager,
-    @CustomTileBoundScope private val boundScope: CoroutineScope,
+    @QSTileScope private val tileScope: CoroutineScope,
     @Background private val backgroundContext: CoroutineContext,
 ) {
 
@@ -51,8 +48,7 @@
         MutableSharedFlow<Tile>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
 
     /** [Tile] updates. [updateTile] to emit a new one. */
-    val tiles: Flow<Tile>
-        get() = customTileRepository.getTiles(user)
+    fun getTiles(user: UserHandle): Flow<Tile> = customTileRepository.getTiles(user)
 
     /**
      * Current [Tile]
@@ -61,10 +57,14 @@
      *   the tile hasn't been updated for the current user. Can happen when this is accessed before
      *   [init] returns.
      */
-    val tile: Tile
-        get() =
-            customTileRepository.getTile(user)
-                ?: throw IllegalStateException("Attempt to get a tile for a wrong user")
+    fun getTile(user: UserHandle): Tile =
+        customTileRepository.getTile(user)
+            ?: throw IllegalStateException("Attempt to get a tile for a wrong user")
+
+    /**
+     * True if the tile is toggleable like a switch and false if it operates as a clickable button.
+     */
+    suspend fun isTileToggleable(): Boolean = customTileRepository.isTileToggleable()
 
     /**
      * Initializes the repository for the current user. Suspends until it's safe to call [tile]
@@ -73,36 +73,36 @@
      * - receive tile update in [updateTile];
      * - restoration happened for a persisted tile.
      */
-    suspend fun init() {
-        launchUpdates()
-        customTileRepository.restoreForTheUserIfNeeded(user, tileServiceManager.isActiveTile)
+    suspend fun initForUser(user: UserHandle) {
+        launchUpdates(user)
+        customTileRepository.restoreForTheUserIfNeeded(user, customTileRepository.isTileActive())
         // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, or
         // tile update.
         customTileRepository.getTiles(user).firstOrNull()
     }
 
-    private fun launchUpdates() {
+    private fun launchUpdates(user: UserHandle) {
         tileUpdates
             .onEach {
                 customTileRepository.updateWithTile(
                     user,
                     it,
-                    tileServiceManager.isActiveTile,
+                    customTileRepository.isTileActive(),
                 )
             }
             .flowOn(backgroundContext)
-            .launchIn(boundScope)
+            .launchIn(tileScope)
         defaultsRepository
             .defaults(user)
             .onEach {
                 customTileRepository.updateWithDefaults(
                     user,
                     it,
-                    tileServiceManager.isActiveTile,
+                    customTileRepository.isTileActive(),
                 )
             }
             .flowOn(backgroundContext)
-            .launchIn(boundScope)
+            .launchIn(tileScope)
     }
 
     /** Updates current [Tile]. Emits a new event in [tiles]. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt
new file mode 100644
index 0000000..4af9854
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.inversion.domain
+
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [ColorInversionTileModel] to [QSTileState]. */
+class ColorInversionTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Theme,
+) : QSTileDataToStateMapper<ColorInversionTileModel> {
+    override fun map(config: QSTileConfig, data: ColorInversionTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            val subtitleArray = resources.getStringArray(R.array.tile_states_inversion)
+
+            if (data.isEnabled) {
+                activationState = QSTileState.ActivationState.ACTIVE
+                secondaryLabel = subtitleArray[2]
+                icon = {
+                    Icon.Loaded(
+                        resources.getDrawable(R.drawable.qs_invert_colors_icon_on, theme),
+                        null
+                    )
+                }
+            } else {
+                activationState = QSTileState.ActivationState.INACTIVE
+                secondaryLabel = subtitleArray[1]
+                icon = {
+                    Icon.Loaded(
+                        resources.getDrawable(R.drawable.qs_invert_colors_icon_off, theme),
+                        null
+                    )
+                }
+            }
+            contentDescription = label
+            supportedActions =
+                setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractor.kt
new file mode 100644
index 0000000..7f3dd3e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionTileDataInteractor.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.inversion.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.accessibility.data.repository.ColorInversionRepository
+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.inversion.domain.model.ColorInversionTileModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Observes color inversion state changes providing the [ColorInversionTileModel]. */
+class ColorInversionTileDataInteractor
+@Inject
+constructor(
+    private val colorInversionRepository: ColorInversionRepository,
+) : QSTileDataInteractor<ColorInversionTileModel> {
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<ColorInversionTileModel> {
+        return colorInversionRepository.isEnabled(user).map { ColorInversionTileModel(it) }
+    }
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
new file mode 100644
index 0000000..43b58c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.inversion.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.ColorInversionRepository
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+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.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles color inversion tile clicks. */
+class ColorInversionUserActionInteractor
+@Inject
+constructor(
+    private val colorInversionRepository: ColorInversionRepository,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<ColorInversionTileModel> {
+
+    override suspend fun handleInput(input: QSTileInput<ColorInversionTileModel>): Unit =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    colorInversionRepository.setIsEnabled(
+                        !data.isEnabled,
+                        user,
+                    )
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.view,
+                        Intent(Settings.ACTION_COLOR_INVERSION_SETTINGS)
+                    )
+                }
+            }
+        }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/model/ColorInversionTileModel.kt
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/model/ColorInversionTileModel.kt
index 6c1f0fc..affaa6d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/model/ColorInversionTileModel.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package android.companion.virtual.camera;
+package com.android.systemui.qs.tiles.impl.inversion.domain.model
 
 /**
- * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
- * VirtualCamera.
- * @hide
+ * Color inversion tile model.
+ *
+ * @param isEnabled is true when the color inversion is enabled;
  */
-parcelable VirtualCameraMetadata;
+@JvmInline value class ColorInversionTileModel(val isEnabled: Boolean)
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 580c421..ef3df48 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
@@ -22,8 +22,8 @@
 
 /**
  * Represents tiles behaviour logic. This ViewModel is a connection between tile view and data
- * layers. All direct inheritors must be added to the [QSTileViewModelInterfaceComplianceTest] class
- * to pass compliance tests.
+ * layers. All direct inheritors must be added to the
+ * [com.android.systemui.qs.tiles.viewmodel.QSTileViewModelTest] class to pass interface tests.
  *
  * All methods of this view model should be considered running on the main thread. This means no
  * synchronous long running operations are permitted in any method.
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
index 4b21e44..f071623 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
@@ -29,11 +29,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.CoreStartable;
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import com.airbnb.lottie.LottieAnimationView;
 import com.airbnb.lottie.LottieDrawable;
@@ -57,7 +58,10 @@
  */
 @SuppressLint("VisibleForTests") // TODO(b/260264542) Migrate away from DeviceStateManagerGlobal
 @SysUISingleton
-public class RearDisplayDialogController implements CoreStartable, CommandQueue.Callbacks {
+public class RearDisplayDialogController implements
+        CoreStartable,
+        ConfigurationController.ConfigurationListener,
+        CommandQueue.Callbacks {
 
     private int[] mFoldedStates;
     private boolean mStartedFolded;
@@ -96,7 +100,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing()
                 && mDialogViewContainer != null) {
             // Refresh the dialog view when configuration is changed.
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt
new file mode 100644
index 0000000..6ab294d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.reardisplay
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+interface RearDisplayModule {
+
+    /** Start RearDisplayDialogController. */
+    @Binds
+    @IntoMap
+    @ClassKey(RearDisplayDialogController::class)
+    abstract fun bindRearDisplayDialogControllerStartable(
+        impl: RearDisplayDialogController
+    ): CoreStartable
+
+    /** Listen to config changes for RearDisplayDialogController. */
+    @Binds
+    @IntoSet
+    fun bindRearDisplayDialogControllerConfigChanges(
+        impl: RearDisplayDialogController
+    ): ConfigurationListener
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index b041f95..4ee65b9 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -23,13 +23,17 @@
 
 import com.android.systemui.CoreStartable;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import java.io.PrintWriter;
 
 /**
  * A proxy to a Recents implementation.
  */
-public class Recents implements CoreStartable, CommandQueue.Callbacks {
+public class Recents implements
+        CoreStartable,
+        ConfigurationController.ConfigurationListener,
+        CommandQueue.Callbacks {
 
     private final Context mContext;
     private final RecentsImplementation mImpl;
@@ -53,7 +57,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         mImpl.onConfigurationChanged(newConfig);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsModule.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsModule.java
index 77a4b9f7..1108917 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsModule.java
@@ -18,14 +18,17 @@
 
 import android.content.Context;
 
-import com.android.systemui.res.R;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.ContextComponentHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.ClassKey;
 import dagger.multibindings.IntoMap;
+import dagger.multibindings.IntoSet;
 
 /**
  * Dagger injection module for {@link RecentsImplementation}
@@ -33,6 +36,28 @@
 @Module
 public abstract class RecentsModule {
 
+    /** Start Recents.  */
+    @Binds
+    @IntoMap
+    @ClassKey(Recents.class)
+    abstract CoreStartable bindRecentsStartable(Recents impl);
+
+    /** Listen to config changes for Recents.  */
+    @Binds
+    @IntoSet
+    abstract ConfigurationListener bindRecentsConfigChanges(Recents impl);
+
+    /** Start ScreenPinningRequest.  */
+    @Binds
+    @IntoMap
+    @ClassKey(ScreenPinningRequest.class)
+    abstract CoreStartable bindScreenPinningRequestStartable(ScreenPinningRequest impl);
+
+    /** Listen to config changes for ScreenPinningRequest.  */
+    @Binds
+    @IntoSet
+    abstract ConfigurationListener bindScreenPinningRequestConfigChanges(ScreenPinningRequest impl);
+
     /**
      * @return The {@link RecentsImplementation} from the config.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
index 3e574e7..2b717cb 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
@@ -54,25 +54,29 @@
 import androidx.annotation.NonNull;
 
 import com.android.systemui.CoreStartable;
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.leak.RotationUtils;
 
+import dagger.Lazy;
+
 import java.util.ArrayList;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 @SysUISingleton
-public class ScreenPinningRequest implements View.OnClickListener,
-        NavigationModeController.ModeChangedListener, CoreStartable {
+public class ScreenPinningRequest implements
+        View.OnClickListener,
+        NavigationModeController.ModeChangedListener,
+        CoreStartable,
+        ConfigurationController.ConfigurationListener {
     private static final String TAG = "ScreenPinningRequest";
 
     private final Context mContext;
@@ -149,7 +153,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         if (mRequestWindow != null) {
             mRequestWindow.onConfigurationChanged();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 5bf44fa..e051df4 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -24,27 +24,63 @@
 import android.content.res.ColorStateList
 import android.graphics.Color
 import android.os.Bundle
+import android.os.UserHandle
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.WindowManager
 import android.widget.Button
 import android.widget.PopupMenu
 import android.widget.Switch
+import androidx.annotation.AnyThread
+import androidx.annotation.MainThread
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
+import com.android.systemui.mediaprojection.SessionCreationSource
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog
+import com.android.systemui.qs.tiles.RecordIssueTile
 import com.android.systemui.res.R
 import com.android.systemui.screenrecord.RecordingService
 import com.android.systemui.screenrecord.ScreenRecordingAudioSource
 import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.concurrent.Executor
 
-class RecordIssueDialogDelegate(
+class RecordIssueDialogDelegate
+@AssistedInject
+constructor(
     private val factory: SystemUIDialog.Factory,
     private val userContextProvider: UserContextProvider,
-    private val onStarted: Runnable
+    private val userTracker: UserTracker,
+    private val flags: FeatureFlagsClassic,
+    @Background private val bgExecutor: Executor,
+    @Main private val mainExecutor: Executor,
+    private val devicePolicyResolver: dagger.Lazy<ScreenCaptureDevicePolicyResolver>,
+    private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
+    private val userFileManager: UserFileManager,
+    @Assisted private val onStarted: Runnable,
 ) : SystemUIDialog.Delegate {
 
+    /** To inject dependencies and allow for easier testing */
+    @AssistedFactory
+    interface Factory {
+        /** Create a dialog object */
+        fun create(onStarted: Runnable): RecordIssueDialogDelegate
+    }
+
     @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch
     private lateinit var issueTypeButton: Button
 
+    @MainThread
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
         dialog.apply {
             setView(LayoutInflater.from(context).inflate(R.layout.record_issue_dialog, null))
@@ -63,17 +99,64 @@
 
     override fun createDialog(): SystemUIDialog = factory.create(this)
 
+    @MainThread
     override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
         dialog.apply {
             window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
             window?.setGravity(Gravity.CENTER)
 
             screenRecordSwitch = requireViewById(R.id.screenrecord_switch)
+            screenRecordSwitch.setOnCheckedChangeListener { _, isEnabled ->
+                onScreenRecordSwitchClicked(context, isEnabled)
+            }
             issueTypeButton = requireViewById(R.id.issue_type_button)
             issueTypeButton.setOnClickListener { onIssueTypeClicked(context) }
         }
     }
 
+    @AnyThread
+    private fun onScreenRecordSwitchClicked(context: Context, isEnabled: Boolean) {
+        if (!isEnabled) return
+
+        bgExecutor.execute {
+            if (
+                flags.isEnabled(WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES) &&
+                    devicePolicyResolver
+                        .get()
+                        .isScreenCaptureCompletelyDisabled(UserHandle.of(userTracker.userId))
+            ) {
+                mainExecutor.execute {
+                    ScreenCaptureDisabledDialog(context).show()
+                    screenRecordSwitch.isChecked = false
+                }
+                return@execute
+            }
+
+            mediaProjectionMetricsLogger.notifyProjectionInitiated(
+                userTracker.userId,
+                SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER
+            )
+
+            if (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)) {
+                val prefs =
+                    userFileManager.getSharedPreferences(
+                        RecordIssueTile.TILE_SPEC,
+                        Context.MODE_PRIVATE,
+                        userTracker.userId
+                    )
+                if (!prefs.getBoolean(HAS_APPROVED_SCREEN_RECORDING, false)) {
+                    mainExecutor.execute {
+                        ScreenCapturePermissionDialogDelegate(factory, prefs).createDialog().apply {
+                            setOnCancelListener { screenRecordSwitch.isChecked = false }
+                            show()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @MainThread
     private fun onIssueTypeClicked(context: Context) {
         val selectedCategory = issueTypeButton.text.toString()
         val popupMenu = PopupMenu(context, issueTypeButton)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/ScreenCapturePermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/ScreenCapturePermissionDialogDelegate.kt
new file mode 100644
index 0000000..de6d3f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/ScreenCapturePermissionDialogDelegate.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.recordissue
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.Gravity
+import android.view.WindowManager
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+const val HAS_APPROVED_SCREEN_RECORDING = "HasApprovedScreenRecord"
+
+class ScreenCapturePermissionDialogDelegate(
+    private val dialogFactory: SystemUIDialog.Factory,
+    private val sharedPreferences: SharedPreferences,
+) : SystemUIDialog.Delegate {
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        dialog.apply {
+            setIcon(R.drawable.ic_screenrecord)
+            setTitle(R.string.screenrecord_permission_dialog_title)
+            setMessage(R.string.screenrecord_permission_dialog_warning_entire_screen)
+            setNegativeButton(R.string.slice_permission_deny) { _, _ -> cancel() }
+            setPositiveButton(R.string.slice_permission_allow) { _, _ ->
+                sharedPreferences.edit().putBoolean(HAS_APPROVED_SCREEN_RECORDING, true).apply()
+                dismiss()
+            }
+            window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
+            window?.setGravity(Gravity.CENTER)
+        }
+    }
+
+    override fun createDialog(): SystemUIDialog = dialogFactory.create(this)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 47518bb..5abb4dd 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.printSection
 import com.android.systemui.util.println
+import dagger.Lazy
 import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -80,8 +81,8 @@
     private val sceneLogger: SceneLogger,
     @FalsingCollectorActual private val falsingCollector: FalsingCollector,
     private val powerInteractor: PowerInteractor,
-    private val simBouncerInteractor: SimBouncerInteractor,
-    private val authenticationInteractor: AuthenticationInteractor,
+    private val simBouncerInteractor: Lazy<SimBouncerInteractor>,
+    private val authenticationInteractor: Lazy<AuthenticationInteractor>,
 ) : CoreStartable {
 
     override fun start() {
@@ -152,7 +153,7 @@
             }
         }
         applicationScope.launch {
-            simBouncerInteractor.isAnySimSecure.collect { isAnySimLocked ->
+            simBouncerInteractor.get().isAnySimSecure.collect { isAnySimLocked ->
                 val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
                 val isUnlocked = deviceEntryInteractor.isUnlocked.value
 
@@ -166,15 +167,17 @@
                     isUnlocked && canSwipeToEnter == false -> {
                         switchToScene(
                             targetSceneKey = SceneKey.Gone,
-                            loggingReason = "All SIM cards unlocked and device already" +
-                                " unlocked and lockscreen doesn't require a swipe to dismiss."
+                            loggingReason =
+                                "All SIM cards unlocked and device already" +
+                                    " unlocked and lockscreen doesn't require a swipe to dismiss."
                         )
                     }
                     else -> {
                         switchToScene(
                             targetSceneKey = SceneKey.Lockscreen,
-                            loggingReason = "All SIM cards unlocked and device still locked" +
-                                " or lockscreen still requires a swipe to dismiss."
+                            loggingReason =
+                                "All SIM cards unlocked and device still locked" +
+                                    " or lockscreen still requires a swipe to dismiss."
                         )
                     }
                 }
@@ -262,7 +265,7 @@
                                     " to swipe up on lockscreen to enter.",
                         )
                     } else if (
-                        authenticationInteractor.getAuthenticationMethod() ==
+                        authenticationInteractor.get().getAuthenticationMethod() ==
                             AuthenticationMethodModel.Sim
                     ) {
                         switchToScene(
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 1156250..8c3e4a5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -14,30 +14,94 @@
  * limitations under the License.
  */
 
+@file:Suppress("NOTHING_TO_INLINE")
+
 package com.android.systemui.scene.shared.flag
 
-import android.content.Context
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.Flags.sceneContainer
 import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flag
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.ResourceBooleanFlag
-import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED
+import com.android.systemui.flags.RefactorFlagUtils
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
-import com.android.systemui.res.R
 import dagger.Module
 import dagger.Provides
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+
+/** Helper for reading or using the scene container flag state. */
+object SceneContainerFlag {
+    /** The flag description -- not an aconfig flag name */
+    const val DESCRIPTION = "SceneContainerFlag"
+
+    inline val isEnabled
+        get() =
+            SCENE_CONTAINER_ENABLED && // mainStaticFlag
+            sceneContainer() && // mainAconfigFlag
+                keyguardBottomAreaRefactor() &&
+                KeyguardShadeMigrationNssl.isEnabled &&
+                MediaInSceneContainerFlag.isEnabled &&
+                ComposeFacade.isComposeAvailable()
+
+    /**
+     * The main static flag, SCENE_CONTAINER_ENABLED. This is an explicit static flag check that
+     * helps with downstream optimizations (like unused code stripping) in builds where aconfig
+     * flags are still writable. Do not remove!
+     */
+    inline fun getMainStaticFlag() =
+        FlagToken("Flags.SCENE_CONTAINER_ENABLED", SCENE_CONTAINER_ENABLED)
+
+    /** The main aconfig flag. */
+    inline fun getMainAconfigFlag() = FlagToken(FLAG_SCENE_CONTAINER, sceneContainer())
+
+    /** The set of secondary flags which must be enabled for scene container to work properly */
+    inline fun getSecondaryFlags(): Sequence<FlagToken> =
+        sequenceOf(
+            FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()),
+            KeyguardShadeMigrationNssl.token,
+            MediaInSceneContainerFlag.token,
+        )
+
+    /** The full set of requirements for SceneContainer */
+    inline fun getAllRequirements(): Sequence<FlagToken> {
+        val composeRequirement =
+            FlagToken("ComposeFacade.isComposeAvailable()", ComposeFacade.isComposeAvailable())
+        return sequenceOf(getMainStaticFlag(), getMainAconfigFlag()) +
+            getSecondaryFlags() +
+            composeRequirement
+    }
+
+    /** Return all dependencies of this flag in pairs where [Pair.first] depends on [Pair.second] */
+    inline fun getFlagDependencies(): Sequence<Pair<FlagToken, FlagToken>> {
+        val mainStaticFlag = getMainStaticFlag()
+        val mainAconfigFlag = getMainAconfigFlag()
+        return sequence {
+            // The static and aconfig flags should be equal; make them co-dependent
+            yield(mainAconfigFlag to mainStaticFlag)
+            yield(mainStaticFlag to mainAconfigFlag)
+            // all other flags depend on the static flag for brevity
+        } + getSecondaryFlags().map { mainStaticFlag to it }
+    }
+
+    /**
+     * 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, DESCRIPTION)
+
+    /**
+     * 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, DESCRIPTION)
+}
 
 /**
  * Defines interface for classes that can check whether the scene container framework feature is
@@ -52,133 +116,25 @@
     fun requirementDescription(): String
 }
 
-class SceneContainerFlagsImpl
-@AssistedInject
-constructor(
-    @Application private val context: Context,
-    private val featureFlagsClassic: FeatureFlagsClassic,
-    @Assisted private val isComposeAvailable: Boolean,
-) : SceneContainerFlags {
-
-    companion object {
-        @VisibleForTesting
-        val classicFlagTokens: List<Flag<Boolean>> =
-            listOf(
-                Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW,
-            )
-    }
-
-    /** The list of requirements, all must be met for the feature to be enabled. */
-    private val requirements =
-        listOf(
-            AconfigFlagMustBeEnabled(
-                flagName = AConfigFlags.FLAG_SCENE_CONTAINER,
-                flagValue = sceneContainer(),
-            ),
-            AconfigFlagMustBeEnabled(
-                flagName = AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
-                flagValue = keyguardBottomAreaRefactor(),
-            ),
-            AconfigFlagMustBeEnabled(
-                flagName = KeyguardShadeMigrationNssl.FLAG_NAME,
-                flagValue = KeyguardShadeMigrationNssl.isEnabled,
-            ),
-            AconfigFlagMustBeEnabled(
-                flagName = MediaInSceneContainerFlag.FLAG_NAME,
-                flagValue = MediaInSceneContainerFlag.isEnabled,
-            ),
-        ) +
-            classicFlagTokens.map { flagToken -> FlagMustBeEnabled(flagToken) } +
-            listOf(
-                ComposeMustBeAvailable(),
-                CompileTimeFlagMustBeEnabled(),
-                ResourceConfigMustBeEnabled()
-            )
+class SceneContainerFlagsImpl : SceneContainerFlags {
 
     override fun isEnabled(): Boolean {
-        // SCENE_CONTAINER_ENABLED is an explicit static flag check that helps with downstream
-        // optimizations, e.g., unused code stripping. Do not remove!
-        return Flags.SCENE_CONTAINER_ENABLED && requirements.all { it.isMet() }
+        return SceneContainerFlag.isEnabled
     }
 
     override fun requirementDescription(): String {
         return buildString {
-            requirements.forEach { requirement ->
+            SceneContainerFlag.getAllRequirements().forEach { requirement ->
                 append('\n')
-                append(if (requirement.isMet()) "    [MET]" else "[NOT MET]")
+                append(if (requirement.isEnabled) "    [MET]" else "[NOT MET]")
                 append(" ${requirement.name}")
             }
         }
     }
-
-    private interface Requirement {
-        val name: String
-
-        fun isMet(): Boolean
-    }
-
-    private inner class ComposeMustBeAvailable : Requirement {
-        override val name = "Jetpack Compose must be available"
-
-        override fun isMet(): Boolean {
-            return isComposeAvailable
-        }
-    }
-
-    private inner class CompileTimeFlagMustBeEnabled : Requirement {
-        override val name = "Flags.SCENE_CONTAINER_ENABLED must be enabled in code"
-
-        override fun isMet(): Boolean {
-            return Flags.SCENE_CONTAINER_ENABLED
-        }
-    }
-
-    private inner class FlagMustBeEnabled<FlagType : Flag<*>>(
-        private val flag: FlagType,
-    ) : Requirement {
-        override val name = "Flag ${flag.name} must be enabled"
-
-        override fun isMet(): Boolean {
-            return when (flag) {
-                is ResourceBooleanFlag -> featureFlagsClassic.isEnabled(flag)
-                is ReleasedFlag -> featureFlagsClassic.isEnabled(flag)
-                is UnreleasedFlag -> featureFlagsClassic.isEnabled(flag)
-                else -> error("Unsupported flag type ${flag.javaClass}")
-            }
-        }
-    }
-
-    private inner class AconfigFlagMustBeEnabled(
-        flagName: String,
-        private val flagValue: Boolean,
-    ) : Requirement {
-        override val name: String = "Aconfig flag $flagName must be enabled"
-
-        override fun isMet(): Boolean {
-            return flagValue
-        }
-    }
-
-    private inner class ResourceConfigMustBeEnabled : Requirement {
-        override val name: String = "R.bool.config_sceneContainerFrameworkEnabled must be true"
-
-        override fun isMet(): Boolean {
-            return context.resources.getBoolean(R.bool.config_sceneContainerFrameworkEnabled)
-        }
-    }
-
-    @AssistedFactory
-    interface Factory {
-        fun create(isComposeAvailable: Boolean): SceneContainerFlagsImpl
-    }
 }
 
 @Module
 object SceneContainerFlagsModule {
 
-    @Provides
-    @SysUISingleton
-    fun impl(factory: SceneContainerFlagsImpl.Factory): SceneContainerFlags {
-        return factory.create(ComposeFacade.isComposeAvailable())
-    }
+    @Provides @SysUISingleton fun impl(): SceneContainerFlags = SceneContainerFlagsImpl()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index a950539..bee3152 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -29,7 +29,7 @@
 import android.view.RemoteAnimationTarget
 import android.view.WindowManager
 import android.view.WindowManagerGlobal
-import com.android.app.tracing.TraceUtils.Companion.launch
+import com.android.app.tracing.coroutines.launch
 import com.android.internal.infra.ServiceConnector
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
index 5cbea90..7130fa1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -11,8 +11,6 @@
 import android.view.animation.AccelerateDecelerateInterpolator
 import androidx.constraintlayout.widget.Guideline
 import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import javax.inject.Inject
 
 /**
@@ -23,7 +21,6 @@
 constructor(
     private val workProfileMessageController: WorkProfileMessageController,
     private val screenshotDetectionController: ScreenshotDetectionController,
-    private val featureFlags: FeatureFlags,
 ) {
     private lateinit var container: ViewGroup
     private lateinit var guideline: Guideline
@@ -63,10 +60,8 @@
 
     fun onScreenshotTaken(screenshot: ScreenshotData) {
         val workProfileData = workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
-        var notifiedApps: List<CharSequence> = listOf()
-        if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
-            notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
-        }
+        var notifiedApps: List<CharSequence> =
+            screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
 
         // If work profile first run needs to show, bias towards that, otherwise show screenshot
         // detection notification if needed.
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index f56f416..3081f89 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -20,7 +20,7 @@
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.app.tracing.TraceUtils.Companion.launch
+import com.android.app.tracing.coroutines.launch
 import kotlinx.coroutines.CoroutineScope
 import java.util.function.Consumer
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index 713ede6..86f6523 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.shade.ShadeExpansionStateManager
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
-import com.android.app.tracing.TraceUtils.Companion.launch
+import com.android.app.tracing.coroutines.launch
 import kotlinx.coroutines.withContext
 
 /** Provides state from the main SystemUI process on behalf of the Screenshot process. */
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
index 38d00f7..238a552 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
@@ -18,7 +18,7 @@
 
 import android.media.MediaPlayer
 import android.util.Log
-import com.android.app.tracing.TraceUtils.Companion.async
+import com.android.app.tracing.coroutines.async
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.google.errorprone.annotations.CanIgnoreReturnValue
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index f6c25e0..e464fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -5,7 +5,7 @@
 import android.util.Log
 import android.view.Display
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.app.tracing.TraceUtils.Companion.launch
+import com.android.app.tracing.coroutines.launch
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 9f416bb..f2fa0ef 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -135,6 +135,10 @@
         val filter = IntentFilter().apply {
             addAction(Intent.ACTION_LOCALE_CHANGED)
             addAction(Intent.ACTION_USER_INFO_CHANGED)
+            addAction(Intent.ACTION_PROFILE_ADDED)
+            addAction(Intent.ACTION_PROFILE_REMOVED)
+            addAction(Intent.ACTION_PROFILE_AVAILABLE)
+            addAction(Intent.ACTION_PROFILE_UNAVAILABLE)
             // These get called when a managed profile goes in or out of quiet mode.
             addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
             addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
@@ -157,7 +161,11 @@
             Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
             Intent.ACTION_MANAGED_PROFILE_ADDED,
             Intent.ACTION_MANAGED_PROFILE_REMOVED,
-            Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> {
+            Intent.ACTION_MANAGED_PROFILE_UNLOCKED,
+            Intent.ACTION_PROFILE_ADDED,
+            Intent.ACTION_PROFILE_REMOVED,
+            Intent.ACTION_PROFILE_AVAILABLE,
+            Intent.ACTION_PROFILE_UNAVAILABLE -> {
                 handleProfilesChanged()
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index d13edf0..d382b7a 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -21,6 +21,8 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.view.WindowManagerPolicyConstants.EXTRA_FROM_BRIGHTNESS_KEY;
 
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+
 import android.app.Activity;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -87,6 +89,17 @@
         if (mShadeInteractor.isQsExpanded().getValue()) {
             finish();
         }
+
+        View view = findViewById(R.id.brightness_mirror_container);
+        if (view != null) {
+            collectFlow(view, mShadeInteractor.isQsExpanded(), this::onShadeStateChange);
+        }
+    }
+
+    private void onShadeStateChange(boolean isQsExpanded) {
+        if (isQsExpanded) {
+            requestFinish();
+        }
     }
 
     private void setWindowAttributes() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
new file mode 100644
index 0000000..ab69acb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.content.Context
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.compose.ComposeFacade.createCommunalContainer
+import com.android.systemui.compose.ComposeFacade.isComposeAvailable
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.collectFlow
+import javax.inject.Inject
+
+/**
+ * Controller that's responsible for the glanceable hub container view and its touch handling.
+ *
+ * This will be used until the glanceable hub is integrated into Flexiglass.
+ */
+class GlanceableHubContainerController
+@Inject
+constructor(
+    private val communalInteractor: CommunalInteractor,
+    private val communalViewModel: CommunalViewModel,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val shadeInteractor: ShadeInteractor,
+) {
+    /** The container view for the hub. This will not be initialized until [initView] is called. */
+    private lateinit var communalContainerView: View
+
+    /**
+     * The width of the area in which a right edge swipe can open the hub, in pixels. Read from
+     * resources when [initView] is called.
+     */
+    private var edgeSwipeRegionWidth: Int = 0
+
+    /**
+     * True if we are currently tracking a gesture for opening the hub that started in the edge
+     * swipe region.
+     */
+    private var isTrackingOpenGesture = false
+
+    /**
+     * True if the hub UI is fully open, meaning it should receive touch input.
+     *
+     * Tracks [CommunalInteractor.isCommunalShowing].
+     */
+    private var hubShowing = false
+
+    /**
+     * True if either the primary or alternate bouncer are open, meaning the hub should not receive
+     * any touch input.
+     *
+     * Tracks [KeyguardTransitionInteractor.isFinishedInState] for [KeyguardState.isBouncerState].
+     */
+    private var anyBouncerShowing = false
+
+    /**
+     * True if the shade is fully expanded, meaning the hub should not receive any touch input.
+     *
+     * Tracks [ShadeInteractor.isAnyFullyExpanded].
+     */
+    private var shadeShowing = false
+
+    /** Returns true if the glanceable hub is enabled and the container view can be created. */
+    fun isEnabled(): Boolean {
+        return communalInteractor.isCommunalEnabled && isComposeAvailable()
+    }
+
+    /**
+     * Creates the container view containing the glanceable hub UI.
+     *
+     * @throws RuntimeException if [isEnabled] is false or the view is already initialized
+     */
+    fun initView(
+        context: Context,
+    ): View {
+        return initView(createCommunalContainer(context, communalViewModel))
+    }
+
+    /** Override for testing. */
+    @VisibleForTesting
+    internal fun initView(containerView: View): View {
+        if (!isEnabled()) {
+            throw RuntimeException("Glanceable hub is not enabled")
+        }
+        if (::communalContainerView.isInitialized) {
+            throw RuntimeException("Communal view has already been initialized")
+        }
+
+        communalContainerView = containerView
+
+        edgeSwipeRegionWidth =
+            communalContainerView.resources.getDimensionPixelSize(R.dimen.communal_grid_gutter_size)
+
+        collectFlow(
+            communalContainerView,
+            keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
+            { anyBouncerShowing = it }
+        )
+        collectFlow(
+            communalContainerView,
+            communalInteractor.isCommunalShowing,
+            { hubShowing = it }
+        )
+        collectFlow(
+            communalContainerView,
+            shadeInteractor.isAnyFullyExpanded,
+            { shadeShowing = it }
+        )
+
+        return communalContainerView
+    }
+
+    /**
+     * Notifies the hub container of a touch event. Returns true if it's determined that the touch
+     * should go to the hub container and no one else.
+     *
+     * Special handling is needed because the hub container sits at the lowest z-order in
+     * [NotificationShadeWindowView] and would not normally receive touches. We also cannot use a
+     * [GestureDetector] as the hub container's SceneTransitionLayout is a Compose view that expects
+     * to be fully in control of its own touch handling.
+     */
+    fun onTouchEvent(ev: MotionEvent): Boolean {
+        if (!::communalContainerView.isInitialized) {
+            return false
+        }
+
+        val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
+        val isUp = ev.actionMasked == MotionEvent.ACTION_UP
+        val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
+
+        // TODO(b/315207481): also account for opening animations of shade/bouncer and not just
+        //  fully showing state
+        val hubOccluded = anyBouncerShowing || shadeShowing
+
+        // If the hub is fully visible, send all touch events to it.
+        val communalVisible = hubShowing && !hubOccluded
+        if (communalVisible) {
+            return communalContainerView.dispatchTouchEvent(ev)
+        }
+
+        if (edgeSwipeRegionWidth == 0) {
+            // If the edge region width has not been read yet or whatever reason, don't bother
+            // intercepting touches to open the hub.
+            return false
+        }
+
+        if (!isTrackingOpenGesture && isDown) {
+            val x = ev.rawX
+            val inOpeningSwipeRegion: Boolean =
+                x >= communalContainerView.width - edgeSwipeRegionWidth
+            if (inOpeningSwipeRegion && !hubOccluded) {
+                isTrackingOpenGesture = true
+                return communalContainerView.dispatchTouchEvent(ev)
+            }
+        } else if (isTrackingOpenGesture) {
+            if (isUp || isCancel) {
+                isTrackingOpenGesture = false
+            }
+            return communalContainerView.dispatchTouchEvent(ev)
+        }
+
+        return false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 95f7c94a..17eb3c8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1096,7 +1096,7 @@
             }
 
             @Override
-            public void onPulseExpansionChanged(boolean expandingChanged) {
+            public void onPulseExpansionAmountChanged(boolean expandingChanged) {
                 if (mKeyguardBypassController.getBypassEnabled()) {
                     // Position the notifications while dragging down while pulsing
                     requestScrollerTopPaddingUpdate(false /* animate */);
@@ -1720,9 +1720,12 @@
         }
         // To prevent the weather clock from overlapping with the notification shelf on AOD, we use
         // the small clock here
-        if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf()
-                && hasVisibleNotifications() && isOnAod()) {
-            return SMALL;
+        // With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks
+        if (!migrateClocksToBlueprint()) {
+            if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf()
+                    && hasVisibleNotifications() && isOnAod()) {
+                return SMALL;
+            }
         }
         return LARGE;
     }
@@ -1742,8 +1745,9 @@
         } else {
             layout = mNotificationContainerParent;
         }
+
         if (migrateClocksToBlueprint()) {
-            mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
+            mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered);
         } else {
             mKeyguardStatusViewController.updateAlignment(
                     layout, mSplitShadeEnabled, shouldBeCentered, animate);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 73537ed..3cf468f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -42,9 +42,6 @@
 import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.communal.data.repository.CommunalRepository;
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
-import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
 import com.android.systemui.dock.DockManager;
@@ -57,8 +54,13 @@
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler;
+import com.android.systemui.keyguard.ui.binder.AlternateBouncerViewBinder;
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
 import com.android.systemui.log.BouncerLogger;
+import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
 import com.android.systemui.statusbar.DragDownHelper;
@@ -67,6 +69,7 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.gesture.TapGestureDetector;
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
@@ -79,14 +82,19 @@
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.SystemClock;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.Optional;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.ExperimentalCoroutinesApi;
+
 /**
  * Controller for {@link NotificationShadeWindowView}.
  */
@@ -108,14 +116,14 @@
     private final PulsingGestureListener mPulsingGestureListener;
     private final LockscreenHostedDreamGestureListener mLockscreenHostedDreamGestureListener;
     private final NotificationInsetsController mNotificationInsetsController;
-    private final CommunalViewModel mCommunalViewModel;
-    private final CommunalRepository mCommunalRepository;
     private final boolean mIsTrackpadCommonEnabled;
     private final FeatureFlagsClassic mFeatureFlagsClassic;
     private final SysUIKeyEventHandler mSysUIKeyEventHandler;
     private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final QuickSettingsController mQuickSettingsController;
+    private final GlanceableHubContainerController
+            mGlanceableHubContainerController;
     private GestureDetector mPulsingWakeupGestureHandler;
     private GestureDetector mDreamingWakeupGestureHandler;
     private View mBrightnessMirror;
@@ -152,6 +160,7 @@
             };
     private final SystemClock mClock;
 
+    @ExperimentalCoroutinesApi
     @Inject
     public NotificationShadeWindowViewController(
             LockscreenShadeTransitionController transitionController,
@@ -183,8 +192,7 @@
             KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
-            CommunalViewModel communalViewModel,
-            CommunalRepository communalRepository,
+            GlanceableHubContainerController glanceableHubContainerController,
             NotificationLaunchAnimationInteractor notificationLaunchAnimationInteractor,
             FeatureFlagsClassic featureFlagsClassic,
             SystemClock clock,
@@ -194,7 +202,13 @@
             QuickSettingsController quickSettingsController,
             PrimaryBouncerInteractor primaryBouncerInteractor,
             AlternateBouncerInteractor alternateBouncerInteractor,
-            SelectedUserInteractor selectedUserInteractor) {
+            SelectedUserInteractor selectedUserInteractor,
+            Lazy<JavaAdapter> javaAdapter,
+            Lazy<AlternateBouncerViewModel> alternateBouncerViewModel,
+            Lazy<FalsingManager> falsingManager,
+            Lazy<SwipeUpAnywhereGestureHandler> swipeUpAnywhereGestureHandler,
+            Lazy<TapGestureDetector> tapGestureDetector,
+            Lazy<AlternateBouncerUdfpsIconViewModel> alternateBouncerUdfpsIconViewModel) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -217,8 +231,7 @@
         mPulsingGestureListener = pulsingGestureListener;
         mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener;
         mNotificationInsetsController = notificationInsetsController;
-        mCommunalViewModel = communalViewModel;
-        mCommunalRepository = communalRepository;
+        mGlanceableHubContainerController = glanceableHubContainerController;
         mIsTrackpadCommonEnabled = featureFlagsClassic.isEnabled(TRACKPAD_GESTURE_COMMON);
         mFeatureFlagsClassic = featureFlagsClassic;
         mSysUIKeyEventHandler = sysUIKeyEventHandler;
@@ -240,6 +253,25 @@
                 featureFlagsClassic,
                 selectedUserInteractor);
 
+        if (DeviceEntryUdfpsRefactor.isEnabled()) {
+            AlternateBouncerViewBinder.bind(
+                    mView.findViewById(R.id.alternate_bouncer),
+                    alternateBouncerViewModel.get(),
+                    falsingManager.get(),
+                    swipeUpAnywhereGestureHandler.get(),
+                    tapGestureDetector.get(),
+                    alternateBouncerUdfpsIconViewModel.get()
+            );
+            javaAdapter.get().alwaysCollectFlow(
+                    alternateBouncerViewModel.get().getForcePluginOpen(),
+                    forcePluginOpen ->
+                            mNotificationShadeWindowController.setForcePluginOpen(
+                                    forcePluginOpen,
+                                    alternateBouncerViewModel.get()
+                            )
+            );
+        }
+
         collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(),
                 mLockscreenToDreamingTransition);
         collectFlow(
@@ -347,6 +379,10 @@
 
                 mFalsingCollector.onTouchEvent(ev);
                 mPulsingWakeupGestureHandler.onTouchEvent(ev);
+
+                if (mGlanceableHubContainerController.onTouchEvent(ev)) {
+                    return logDownDispatch(ev, "dispatched to glanceable hub container", true);
+                }
                 if (mDreamingWakeupGestureHandler != null
                         && mDreamingWakeupGestureHandler.onTouchEvent(ev)) {
                     return logDownDispatch(ev, "dream wakeup gesture handled", true);
@@ -587,14 +623,13 @@
     }
 
     /**
-     * Sets up the communal hub UI if the {@link com.android.systemui.Flags#FLAG_COMMUNAL_HUB} flag
-     * is enabled.
+     * Sets up the glanceable hub UI if the {@link com.android.systemui.Flags#FLAG_COMMUNAL_HUB}
+     * flag is enabled.
      *
-     * The layout lives in {@link R.id.communal_ui_container}.
+     * The layout lives in {@link R.id.communal_ui_stub}.
      */
     public void setupCommunalHubLayout() {
-        if (!mCommunalRepository.isCommunalEnabled()
-                || !ComposeFacade.INSTANCE.isComposeAvailable()) {
+        if (!mGlanceableHubContainerController.isEnabled()) {
             return;
         }
 
@@ -602,8 +637,8 @@
         View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub);
         int index = mView.indexOfChild(communalPlaceholder);
         mView.removeView(communalPlaceholder);
-        mView.addView(ComposeFacade.INSTANCE.createCommunalContainer(mView.getContext(),
-                mCommunalViewModel), index);
+
+        mView.addView(mGlanceableHubContainerController.initView(mView.getContext()), index);
     }
 
     private boolean didNotificationPanelInterceptEvent(MotionEvent ev) {
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index d26fded1..2f58b35 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -41,14 +41,14 @@
         const val DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN = "dream_weather_smartspace_data_plugin"
 
         /**
-         * The dream smartspace target filter.
+         * The target filter for smartspace over lockscreen.
          */
-        const val DREAM_SMARTSPACE_TARGET_FILTER = "dream_smartspace_target_filter"
+        const val LOCKSCREEN_SMARTSPACE_TARGET_FILTER = "lockscreen_smartspace_target_filter"
 
         /**
-         * The precondition for dream smartspace
+         * The precondition for smartspace over lockscreen
          */
-        const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition"
+        const val LOCKSCREEN_SMARTSPACE_PRECONDITION = "lockscreen_smartspace_precondition"
 
         /**
          * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
@@ -67,7 +67,7 @@
     }
 
     @BindsOptionalOf
-    @Named(DREAM_SMARTSPACE_TARGET_FILTER)
+    @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER)
     abstract fun optionalDreamSmartspaceTargetFilter(): SmartspaceTargetFilter?
 
     @BindsOptionalOf
@@ -79,7 +79,7 @@
     abstract fun optionalDreamWeatherSmartspaceDataPlugin(): BcSmartspaceDataPlugin?
 
     @Binds
-    @Named(DREAM_SMARTSPACE_PRECONDITION)
+    @Named(LOCKSCREEN_SMARTSPACE_PRECONDITION)
     abstract fun bindSmartspacePrecondition(
         lockscreenPrecondition: LockscreenPrecondition?
     ): SmartspacePrecondition?
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenAndDreamTargetFilter.kt b/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenAndDreamTargetFilter.kt
rename to packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt
index aa4cf75..5fb85e5 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenAndDreamTargetFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt
@@ -31,10 +31,10 @@
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
-/**
- * {@link SmartspaceTargetFilter} for smartspace targets that show above the lockscreen and dreams.
- */
-class LockscreenAndDreamTargetFilter @Inject constructor(
+/** {@link SmartspaceTargetFilter} for smartspace targets that show above the lockscreen. */
+class LockscreenTargetFilter
+@Inject
+constructor(
     private val secureSettings: SecureSettings,
     private val userTracker: UserTracker,
     private val execution: Execution,
@@ -60,12 +60,13 @@
             }
         }
 
-    private val settingsObserver = object : ContentObserver(handler) {
-        override fun onChange(selfChange: Boolean, uri: Uri?) {
-            execution.assertIsMainThread()
-            updateUserContentSettings()
+    private val settingsObserver =
+        object : ContentObserver(handler) {
+            override fun onChange(selfChange: Boolean, uri: Uri?) {
+                execution.assertIsMainThread()
+                updateUserContentSettings()
+            }
         }
-    }
 
     private var managedUserHandle: UserHandle? = null
 
@@ -79,10 +80,10 @@
         userTracker.addCallback(userTrackerCallback, uiExecutor)
 
         contentResolver.registerContentObserver(
-                secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
-                true,
-                settingsObserver,
-                UserHandle.USER_ALL
+            secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
+            true,
+            settingsObserver,
+            UserHandle.USER_ALL
         )
 
         updateUserContentSettings()
@@ -110,7 +111,7 @@
                 // Only the primary user can have an associated managed profile, so only show
                 // content for the managed profile if the primary user is active
                 userTracker.userHandle.identifier == UserHandle.USER_SYSTEM &&
-                        (!t.isSensitive || showSensitiveContentForManagedUser)
+                    (!t.isSensitive || showSensitiveContentForManagedUser)
             }
             else -> {
                 false
@@ -118,12 +119,13 @@
         }
     }
 
-    private val userTrackerCallback = object : UserTracker.Callback {
-        override fun onUserChanged(newUser: Int, userContext: Context) {
-            execution.assertIsMainThread()
-            updateUserContentSettings()
+    private val userTrackerCallback =
+        object : UserTracker.Callback {
+            override fun onUserChanged(newUser: Int, userContext: Context) {
+                execution.assertIsMainThread()
+                updateUserContentSettings()
+            }
         }
-    }
 
     private fun getWorkProfileUser(): UserHandle? {
         for (userInfo in userTracker.userProfiles) {
@@ -138,13 +140,13 @@
         val setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS
 
         showSensitiveContentForCurrentUser =
-                secureSettings.getIntForUser(setting, 0, userTracker.userId) == 1
+            secureSettings.getIntForUser(setting, 0, userTracker.userId) == 1
 
         managedUserHandle = getWorkProfileUser()
         val managedId = managedUserHandle?.identifier
         if (managedId != null) {
             showSensitiveContentForManagedUser =
-                    secureSettings.getIntForUser(setting, 0, managedId) == 1
+                secureSettings.getIntForUser(setting, 0, managedId) == 1
         }
 
         listeners.forEach { it.onCriteriaChanged() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index 93c55de..71efbab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -44,6 +44,13 @@
 
     boolean isCurrentProfile(int userId);
 
+    /**
+     *
+     * @param userId user Id
+     * @return true if user profile is running.
+     */
+    boolean isProfileAvailable(int userId);
+
     /** Adds a listener to be notified when the current user changes. */
     void addUserChangedListener(UserChangedListener listener);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 05c3839..633510d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -461,6 +461,13 @@
         }
     }
 
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    public boolean isProfileAvailable(int userId) {
+        synchronized (mLock) {
+            return mUserManager.isUserRunning(userId);
+        }
+    }
+
     private void setShowLockscreenNotifications(boolean show) {
         mShowLockscreenNotifications = show;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index 843a454..984fcad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -27,7 +27,6 @@
 import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -59,6 +58,7 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.ContrastColorUtil;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.NotificationContentDescription;
 import com.android.systemui.statusbar.notification.NotificationDozeHelper;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
@@ -350,9 +350,22 @@
     }
 
     public void setNotification(StatusBarNotification notification) {
-        mNotification = notification;
+        CharSequence contentDescription = null;
         if (notification != null) {
-            setContentDescription(notification.getNotification());
+            contentDescription = NotificationContentDescription
+                    .contentDescForNotification(mContext, notification.getNotification());
+        }
+        setNotification(notification, contentDescription);
+    }
+
+    /**
+     * Sets the notification with a pre-set content description.
+     */
+    public void setNotification(@Nullable StatusBarNotification notification,
+            @Nullable CharSequence notificationContentDescription) {
+        mNotification = notification;
+        if (!TextUtils.isEmpty(notificationContentDescription)) {
+            setContentDescription(notificationContentDescription);
         }
         maybeUpdateIconScaleDimens();
     }
@@ -621,15 +634,6 @@
         mNumberBackground.setBounds(w-dw, h-dh, w, h);
     }
 
-    private void setContentDescription(Notification notification) {
-        if (notification != null) {
-            String d = contentDescForNotification(mContext, notification);
-            if (!TextUtils.isEmpty(d)) {
-                setContentDescription(d);
-            }
-        }
-    }
-
     @Override
     public String toString() {
         return "StatusBarIconView("
@@ -647,35 +651,6 @@
         return mSlot;
     }
 
-
-    public static String contentDescForNotification(Context c, Notification n) {
-        String appName = "";
-        try {
-            Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
-            appName = builder.loadHeaderAppName();
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Unable to recover builder", e);
-            // Trying to get the app name from the app info instead.
-            ApplicationInfo appInfo = n.extras.getParcelable(
-                    Notification.EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class);
-            if (appInfo != null) {
-                appName = String.valueOf(appInfo.loadLabel(c.getPackageManager()));
-            }
-        }
-
-        CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
-        CharSequence text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
-        CharSequence ticker = n.tickerText;
-
-        // Some apps just put the app name into the title
-        CharSequence titleOrText = TextUtils.equals(title, appName) ? text : title;
-
-        CharSequence desc = !TextUtils.isEmpty(titleOrText) ? titleOrText
-                : !TextUtils.isEmpty(ticker) ? ticker : "";
-
-        return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
-    }
-
     /**
      * Set the color that is used to draw decoration like the overflow dot. This will not be applied
      * to the drawable.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt
new file mode 100644
index 0000000..e7012ea51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationContentDescription.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("NotificationContentDescription")
+
+package com.android.systemui.statusbar.notification
+
+import android.app.Notification
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.text.TextUtils
+import android.util.Log
+import androidx.annotation.MainThread
+import com.android.systemui.res.R
+
+/**
+ * Returns accessibility content description for a given notification.
+ *
+ * NOTE: This is a relatively slow call.
+ */
+@MainThread
+fun contentDescForNotification(c: Context, n: Notification?): CharSequence {
+    var appName = ""
+    try {
+        val builder = Notification.Builder.recoverBuilder(c, n)
+        appName = builder.loadHeaderAppName()
+    } catch (e: RuntimeException) {
+        Log.e("ContentDescription", "Unable to recover builder", e)
+        // Trying to get the app name from the app info instead.
+        val appInfo =
+            n?.extras?.getParcelable(
+                Notification.EXTRA_BUILDER_APPLICATION_INFO,
+                ApplicationInfo::class.java
+            )
+        if (appInfo != null) {
+            appName = appInfo.loadLabel(c.packageManager).toString()
+        }
+    }
+    val title = n?.extras?.getCharSequence(Notification.EXTRA_TITLE)
+    val text = n?.extras?.getCharSequence(Notification.EXTRA_TEXT)
+    val ticker = n?.tickerText
+
+    // Some apps just put the app name into the title
+    val titleOrText = if (TextUtils.equals(title, appName)) text else title
+    val desc =
+        if (!TextUtils.isEmpty(titleOrText)) titleOrText
+        else if (!TextUtils.isEmpty(ticker)) ticker else ""
+    return c.getString(R.string.accessibility_desc_notification_icon, appName, desc)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 8d1e8d0..0c67279 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -20,9 +20,9 @@
 import android.view.animation.Interpolator
 import androidx.annotation.VisibleForTesting
 import androidx.core.animation.ObjectAnimator
-import com.android.systemui.Dumpable
 import com.android.app.animation.Interpolators
 import com.android.app.animation.InterpolatorsAndroidX
+import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -31,6 +31,7 @@
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -206,8 +207,15 @@
             val nowExpanding = isPulseExpanding()
             val changed = nowExpanding != pulseExpanding
             pulseExpanding = nowExpanding
-            for (listener in wakeUpListeners) {
-                listener.onPulseExpansionChanged(changed)
+            if (!NotificationIconContainerRefactor.isEnabled) {
+                for (listener in wakeUpListeners) {
+                    listener.onPulseExpansionAmountChanged(changed)
+                }
+            }
+            if (changed) {
+                for (listener in wakeUpListeners) {
+                    listener.onPulseExpandingChanged(pulseExpanding)
+                }
             }
         }
     }
@@ -620,13 +628,20 @@
          *
          * @param expandingChanged if the user has started or stopped expanding
          */
-        fun onPulseExpansionChanged(expandingChanged: Boolean) {}
+        @Deprecated(
+            message = "Use onPulseExpandedChanged instead.",
+            replaceWith = ReplaceWith("onPulseExpandedChanged"),
+        )
+        fun onPulseExpansionAmountChanged(expandingChanged: Boolean) {}
 
         /**
          * Called when the animator started by [scheduleDelayedDozeAmountAnimation] begins running
          * after the start delay, or after it ends/is cancelled.
          */
         fun onDelayedDozeAmountAnimationRunning(running: Boolean) {}
+
+        /** Called whenever a pulse has started or stopped expanding. */
+        fun onPulseExpandingChanged(isPulseExpanding: Boolean) {}
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 3e9c6fb..3b48b39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -3,10 +3,8 @@
 import android.util.FloatProperty
 import android.view.View
 import androidx.annotation.FloatRange
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.RefactorFlag
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
 import com.android.systemui.statusbar.notification.stack.AnimationProperties
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import kotlin.math.abs
@@ -42,13 +40,13 @@
     /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
     val topCornerRadius: Float
         get() =
-            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.topCornerRadius
+            if (NotificationsImprovedHunAnimation.isEnabled) roundableState.topCornerRadius
             else topRoundness * maxRadius
 
     /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
     val bottomCornerRadius: Float
         get() =
-            if (roundableState.newHeadsUpAnim.isEnabled) roundableState.bottomCornerRadius
+            if (NotificationsImprovedHunAnimation.isEnabled) roundableState.bottomCornerRadius
             else bottomRoundness * maxRadius
 
     /** Get and update the current radii */
@@ -318,13 +316,10 @@
     internal val targetView: View,
     private val roundable: Roundable,
     maxRadius: Float,
-    featureFlags: FeatureFlags? = null
 ) {
     internal var maxRadius = maxRadius
         private set
 
-    internal val newHeadsUpAnim = RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS, featureFlags)
-
     /** Animatable for top roundness */
     private val topAnimatable = topAnimatable(roundable)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
index cf03d1c..2cc1403 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
@@ -62,8 +62,8 @@
     override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow {
         val listener =
             object : NotificationWakeUpCoordinator.WakeUpListener {
-                override fun onPulseExpansionChanged(expandingChanged: Boolean) {
-                    trySend(expandingChanged)
+                override fun onPulseExpandingChanged(isPulseExpanding: Boolean) {
+                    trySend(isPulseExpanding)
                 }
             }
         trySend(wakeUpCoordinator.isPulseExpanding())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
index afc123f..319b499 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.contentDescForNotification
 import javax.inject.Inject
 
 /**
@@ -36,6 +37,6 @@
     }
 
     fun getIconContentDescription(n: Notification): CharSequence {
-        return StatusBarIconView.contentDescForNotification(context, n)
+        return contentDescForNotification(context, n)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 9e8654a..76e5fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -26,15 +26,15 @@
 import android.util.Log
 import android.view.View
 import android.widget.ImageView
+import com.android.app.tracing.traceSection
 import com.android.internal.statusbar.StatusBarIcon
-import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.InflationException
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.app.tracing.traceSection
 import javax.inject.Inject
 
 /**
@@ -112,15 +112,6 @@
         aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
         aodIcon.setIncreasedSize(true)
 
-        // Construct the centered icon view.
-        val centeredIcon = if (entry.sbn.notification.isMediaNotification) {
-            iconBuilder.createIconView(entry).apply {
-                scaleType = ImageView.ScaleType.CENTER_INSIDE
-            }
-        } else {
-            null
-        }
-
         // Set the icon views' icons
         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
 
@@ -128,10 +119,7 @@
             setIcon(entry, normalIconDescriptor, sbIcon)
             setIcon(entry, sensitiveIconDescriptor, shelfIcon)
             setIcon(entry, sensitiveIconDescriptor, aodIcon)
-            if (centeredIcon != null) {
-                setIcon(entry, normalIconDescriptor, centeredIcon)
-            }
-            entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons)
+            entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, entry.icons)
         } catch (e: InflationException) {
             entry.icons = IconPack.buildEmptyPack(entry.icons)
             throw e
@@ -153,24 +141,22 @@
         entry.icons.peopleAvatarDescriptor = null
 
         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
+        val notificationContentDescription = entry.sbn.notification?.let {
+            iconBuilder.getIconContentDescription(it)
+        }
 
         entry.icons.statusBarIcon?.let {
-            it.notification = entry.sbn
+            it.setNotification(entry.sbn, notificationContentDescription)
             setIcon(entry, normalIconDescriptor, it)
         }
 
         entry.icons.shelfIcon?.let {
-            it.notification = entry.sbn
+            it.setNotification(entry.sbn, notificationContentDescription)
             setIcon(entry, normalIconDescriptor, it)
         }
 
         entry.icons.aodIcon?.let {
-            it.notification = entry.sbn
-            setIcon(entry, sensitiveIconDescriptor, it)
-        }
-
-        entry.icons.centeredIcon?.let {
-            it.notification = entry.sbn
+            it.setNotification(entry.sbn, notificationContentDescription)
             setIcon(entry, sensitiveIconDescriptor, it)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
index 054e381..442c097 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
@@ -31,7 +31,6 @@
     @Nullable private final StatusBarIconView mStatusBarIcon;
     @Nullable private final StatusBarIconView mShelfIcon;
     @Nullable private final StatusBarIconView mAodIcon;
-    @Nullable private final StatusBarIconView mCenteredIcon;
 
     @Nullable private StatusBarIcon mSmallIconDescriptor;
     @Nullable private StatusBarIcon mPeopleAvatarDescriptor;
@@ -43,7 +42,7 @@
      * haven't been inflated yet or there was an error while inflating them).
      */
     public static IconPack buildEmptyPack(@Nullable IconPack fromSource) {
-        return new IconPack(false, null, null, null, null, fromSource);
+        return new IconPack(false, null, null, null, fromSource);
     }
 
     /**
@@ -53,9 +52,8 @@
             @NonNull StatusBarIconView statusBarIcon,
             @NonNull StatusBarIconView shelfIcon,
             @NonNull StatusBarIconView aodIcon,
-            @Nullable StatusBarIconView centeredIcon,
             @Nullable IconPack source) {
-        return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, centeredIcon, source);
+        return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, source);
     }
 
     private IconPack(
@@ -63,12 +61,10 @@
             @Nullable StatusBarIconView statusBarIcon,
             @Nullable StatusBarIconView shelfIcon,
             @Nullable StatusBarIconView aodIcon,
-            @Nullable StatusBarIconView centeredIcon,
             @Nullable IconPack source) {
         mAreIconsAvailable = areIconsAvailable;
         mStatusBarIcon = statusBarIcon;
         mShelfIcon = shelfIcon;
-        mCenteredIcon = centeredIcon;
         mAodIcon = aodIcon;
         if (source != null) {
             mIsImportantConversation = source.mIsImportantConversation;
@@ -91,11 +87,6 @@
         return mShelfIcon;
     }
 
-    @Nullable
-    public StatusBarIconView getCenteredIcon() {
-        return mCenteredIcon;
-    }
-
     /** The version of the icon that's shown when pulsing (in AOD). */
     @Nullable
     public StatusBarIconView getAodIcon() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
index 30e2f0e0..9215568 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
 import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
@@ -37,10 +38,12 @@
 constructor(
     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     private val bubbles: Optional<Bubbles>,
+    private val headsUpNotificationIconInteractor: HeadsUpNotificationIconInteractor,
     private val keyguardViewStateRepository: NotificationsKeyguardViewStateRepository,
 ) {
     /** Returns a subset of all active notifications based on the supplied filtration parameters. */
     fun filteredNotifSet(
+        forceShowHeadsUp: Boolean = false,
         showAmbient: Boolean = true,
         showLowPriority: Boolean = true,
         showDismissed: Boolean = true,
@@ -49,18 +52,21 @@
     ): Flow<Set<ActiveNotificationModel>> {
         return combine(
             activeNotificationsInteractor.topLevelRepresentativeNotifications,
+            headsUpNotificationIconInteractor.isolatedNotification,
             keyguardViewStateRepository.areNotificationsFullyHidden,
-        ) { notifications, notifsFullyHidden ->
+        ) { notifications, isolatedNotifKey, notifsFullyHidden ->
             notifications
                 .asSequence()
                 .filter { model: ActiveNotificationModel ->
                     shouldShowNotificationIcon(
                         model = model,
+                        forceShowHeadsUp = forceShowHeadsUp,
                         showAmbient = showAmbient,
                         showLowPriority = showLowPriority,
                         showDismissed = showDismissed,
                         showRepliedMessages = showRepliedMessages,
                         showPulsing = showPulsing,
+                        isolatedNotifKey = isolatedNotifKey,
                         notifsFullyHidden = notifsFullyHidden,
                     )
                 }
@@ -70,14 +76,17 @@
 
     private fun shouldShowNotificationIcon(
         model: ActiveNotificationModel,
+        forceShowHeadsUp: Boolean,
         showAmbient: Boolean,
         showLowPriority: Boolean,
         showDismissed: Boolean,
         showRepliedMessages: Boolean,
         showPulsing: Boolean,
+        isolatedNotifKey: String?,
         notifsFullyHidden: Boolean,
     ): Boolean {
         return when {
+            forceShowHeadsUp && model.key == isolatedNotifKey -> true
             !showAmbient && model.isAmbient -> false
             !showLowPriority && model.isSilent -> false
             !showDismissed && model.isRowDismissed -> false
@@ -118,6 +127,7 @@
     val statusBarNotifs: Flow<Set<ActiveNotificationModel>> =
         settingsRepository.showSilentStatusIcons.flatMapLatest { showSilentIcons ->
             iconsInteractor.filteredNotifSet(
+                forceShowHeadsUp = true,
                 showAmbient = false,
                 showLowPriority = showSilentIcons,
                 showDismissed = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
new file mode 100644
index 0000000..d7c29f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.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.icon.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+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.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerAlwaysOnDisplayViewModel]. */
+class NotificationIconContainerAlwaysOnDisplayViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
+    private val keyguardRootViewModel: KeyguardRootViewModel,
+    private val configuration: ConfigurationState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val screenOffAnimationController: ScreenOffAnimationController,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val viewStore: AlwaysOnDisplayNotificationIconViewStore,
+) {
+    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                launch {
+                    NotificationIconContainerViewBinder.bind(
+                        view = view,
+                        viewModel = viewModel,
+                        configuration = configuration,
+                        systemBarUtilsState = systemBarUtilsState,
+                        failureTracker = failureTracker,
+                        viewStore = viewStore,
+                    )
+                }
+                launch {
+                    KeyguardRootViewBinder.bindAodNotifIconVisibility(
+                        view = view,
+                        isVisible = keyguardRootViewModel.isNotifIconContainerVisible,
+                        configuration = configuration,
+                        screenOffAnimationController = screenOffAnimationController,
+                    )
+                }
+            }
+        }
+    }
+}
+
+/** [IconViewStore] for the always-on display. */
+class AlwaysOnDisplayNotificationIconViewStore
+@Inject
+constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
new file mode 100644
index 0000000..783488af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.viewbinder
+
+import com.android.systemui.common.ui.ConfigurationState
+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.viewbinder.NotificationIconContainerViewBinder.bindIcons
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerShelfViewModel]. */
+class NotificationIconContainerShelfViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerShelfViewModel,
+    private val configuration: ConfigurationState,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val viewStore: ShelfNotificationIconViewStore,
+) {
+    suspend fun bind(view: NotificationIconContainer) {
+        viewModel.icons.bindIcons(
+            view,
+            configuration,
+            systemBarUtilsState,
+            notifyBindingFailures = { failureTracker.shelfFailures = it },
+            viewStore,
+        )
+    }
+}
+
+/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
+class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
new file mode 100644
index 0000000..8e089b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.lifecycle.repeatWhenAttached
+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.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerStatusBarViewModel]. */
+class NotificationIconContainerStatusBarViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerStatusBarViewModel,
+    private val configuration: ConfigurationState,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val viewStore: StatusBarNotificationIconViewStore,
+) {
+    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                NotificationIconContainerViewBinder.bind(
+                    view = view,
+                    viewModel = viewModel,
+                    configuration = configuration,
+                    systemBarUtilsState = systemBarUtilsState,
+                    failureTracker = failureTracker,
+                    viewStore = viewStore,
+                )
+            }
+        }
+    }
+}
+
+/** [IconViewStore] for the status bar. */
+class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
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 e1e30e1..b76cdb8 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
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
 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.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
@@ -45,7 +44,6 @@
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
-import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
@@ -56,42 +54,6 @@
 
 /** Binds a view-model to a [NotificationIconContainer]. */
 object NotificationIconContainerViewBinder {
-    @JvmStatic
-    fun bindWhileAttached(
-        view: NotificationIconContainer,
-        viewModel: NotificationIconContainerShelfViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
-        failureTracker: StatusBarIconViewBindingFailureTracker,
-        viewStore: IconViewStore,
-    ): DisposableHandle {
-        return view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.icons.bindIcons(
-                    view,
-                    configuration,
-                    systemBarUtilsState,
-                    notifyBindingFailures = { failureTracker.shelfFailures = it },
-                    viewStore,
-                )
-            }
-        }
-    }
-
-    @JvmStatic
-    fun bindWhileAttached(
-        view: NotificationIconContainer,
-        viewModel: NotificationIconContainerStatusBarViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
-        failureTracker: StatusBarIconViewBindingFailureTracker,
-        viewStore: IconViewStore,
-    ): DisposableHandle =
-        view.repeatWhenAttached {
-            lifecycleScope.launch {
-                bind(view, viewModel, configuration, systemBarUtilsState, failureTracker, viewStore)
-            }
-        }
 
     suspend fun bind(
         view: NotificationIconContainer,
@@ -215,7 +177,7 @@
      * given `iconKey`. The parent [Job] of this coroutine will be cancelled automatically when the
      * view is to be unbound.
      */
-    private suspend fun Flow<NotificationIconsViewData>.bindIcons(
+    suspend fun Flow<NotificationIconsViewData>.bindIcons(
         view: NotificationIconContainer,
         configuration: ConfigurationState,
         systemBarUtilsState: SystemBarUtilsState,
@@ -275,7 +237,7 @@
 
                 // Add and bind.
                 val toAdd: Sequence<String> = iconsDiff.added.asSequence() + failedBindings.toList()
-                for ((idx, notifKey) in toAdd.withIndex()) {
+                for (notifKey in toAdd) {
                     // Lookup the StatusBarIconView from the store.
                     val sbiv = viewStore.iconView(notifKey)
                     if (sbiv == null) {
@@ -294,7 +256,7 @@
                         // added again.
                         removeTransientView(sbiv)
                     }
-                    view.addView(sbiv, idx)
+                    view.addView(sbiv)
                     boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
                     boundViewsByNotifKey[notifKey] =
                         Pair(
@@ -377,24 +339,14 @@
     }
 
     @ColorInt private const val DEFAULT_AOD_ICON_COLOR = Color.WHITE
-    private const val TAG =  "NotifIconContainerViewBinder"
+    private const val TAG = "NotifIconContainerViewBinder"
 }
 
-/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
-class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
-
-/** [IconViewStore] for the always-on display. */
-class AlwaysOnDisplayNotificationIconViewStore
-@Inject
-constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
-
-/** [IconViewStore] for the status bar. */
-class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
-
-private fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
+/**
+ * Convenience builder for [IconViewStore] that uses [block] to extract the relevant
+ * [StatusBarIconView] from an [IconPack] stored inside of the [NotifCollection].
+ */
+fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
     IconViewStore { key ->
         getEntry(key)?.icons?.let(block)
     }
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 6e5ac47..d00cd1f 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
@@ -96,8 +96,8 @@
                     iconsViewData.visibleIcons.firstOrNull { it.notifKey == isolatedNotif }
                 }
             }
-            .pairwise(initialValue = null)
             .distinctUntilChanged()
+            .pairwise(initialValue = null)
             .sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion ->
                 val animate =
                     when {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt
index 2624363..db33f92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionRefactor.kt
@@ -17,12 +17,17 @@
 package com.android.systemui.statusbar.notification.interruption
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the visual interruptions refactor flag state. */
 object VisualInterruptionRefactor {
     const val FLAG_NAME = Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Whether the refactor is enabled */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 4fe05ec..fca527f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -31,7 +31,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.Interpolator;
-import android.view.animation.PathInterpolator;
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.jank.InteractionJankMonitor;
@@ -44,6 +43,7 @@
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.util.DumpUtilsKt;
@@ -67,7 +67,8 @@
      * The content of the view should start showing at animation progress value of
      * #ALPHA_APPEAR_START_FRACTION.
      */
-    private static final float ALPHA_APPEAR_START_FRACTION = .4f;
+
+    private static final float ALPHA_APPEAR_START_FRACTION = .7f;
     /**
      * The content should show fully with progress at #ALPHA_APPEAR_END_FRACTION
      * The start of the animation is at #ALPHA_APPEAR_START_FRACTION
@@ -86,9 +87,7 @@
      */
     private boolean mActivated;
 
-    private final Interpolator mSlowOutFastInInterpolator;
     private Interpolator mCurrentAppearInterpolator;
-
     NotificationBackgroundView mBackgroundNormal;
     private float mAnimationTranslationY;
     private boolean mDrawingAppearAnimation;
@@ -116,7 +115,6 @@
 
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
         setClipChildren(false);
         setClipToPadding(false);
         updateColors();
@@ -400,12 +398,16 @@
             mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
             targetValue = 1.0f;
         } else {
-            mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
+            mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE;
             targetValue = 0.0f;
         }
         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
                 targetValue);
-        mAppearAnimator.setInterpolator(Interpolators.LINEAR);
+        if (NotificationsImprovedHunAnimation.isEnabled()) {
+            mAppearAnimator.setInterpolator(mCurrentAppearInterpolator);
+        } else {
+            mAppearAnimator.setInterpolator(Interpolators.LINEAR);
+        }
         mAppearAnimator.setDuration(
                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
         mAppearAnimator.addUpdateListener(animation -> {
@@ -502,8 +504,9 @@
     }
 
     private void updateAppearRect() {
-        float interpolatedFraction = mCurrentAppearInterpolator.getInterpolation(
-                mAppearAnimationFraction);
+        float interpolatedFraction =
+                NotificationsImprovedHunAnimation.isEnabled() ? mAppearAnimationFraction
+                        : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction);
         mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY;
         final int actualHeight = getActualHeight();
         float bottom = actualHeight * interpolatedFraction;
@@ -524,6 +527,7 @@
     }
 
     private float getInterpolatedAppearAnimationFraction() {
+
         if (mAppearAnimationFraction >= 0) {
             return mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction);
         }
@@ -569,7 +573,7 @@
 
     @Override
     public float getTopCornerRadius() {
-        if (mImprovedHunAnimation.isEnabled()) {
+        if (NotificationsImprovedHunAnimation.isEnabled()) {
             return super.getTopCornerRadius();
         }
 
@@ -579,7 +583,7 @@
 
     @Override
     public float getBottomCornerRadius() {
-        if (mImprovedHunAnimation.isEnabled()) {
+        if (NotificationsImprovedHunAnimation.isEnabled()) {
             return super.getBottomCornerRadius();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index b6d4ded..31ca106 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -38,6 +38,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -272,6 +273,13 @@
     private final RefactorFlag mInlineReplyAnimation =
             RefactorFlag.forView(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
 
+    private static final boolean mSimulateSlowMeasure = Compile.IS_DEBUG && RefactorFlag.forView(
+            Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE).isEnabled();
+    private static final String SLOW_MEASURE_SIMULATE_DELAY_PROPERTY =
+            "persist.notifications.extra_measure_delay_ms";
+    private static final int SLOW_MEASURE_SIMULATE_DELAY_MS = mSimulateSlowMeasure ?
+            SystemProperties.getInt(SLOW_MEASURE_SIMULATE_DELAY_PROPERTY, 150) : 0;
+
     // Listener will be called when receiving a long click event.
     // Use #setLongPressPosition to optionally assign positional data with the long press.
     private LongPressListener mLongPressListener;
@@ -505,15 +513,13 @@
     private void setImageViewAnimationRunning(ImageView imageView, boolean running) {
         if (imageView != null) {
             Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AnimationDrawable) {
-                AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
+            if (drawable instanceof AnimationDrawable animationDrawable) {
                 if (running) {
                     animationDrawable.start();
                 } else {
                     animationDrawable.stop();
                 }
-            } else if (drawable instanceof AnimatedVectorDrawable) {
-                AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable;
+            } else if (drawable instanceof AnimatedVectorDrawable animationDrawable) {
                 if (running) {
                     animationDrawable.start();
                 } else {
@@ -1879,9 +1885,26 @@
                     + "heightMeasureSpec=" + MeasureSpec.toString(heightMeasureSpec) + ")");
         }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (Compile.IS_DEBUG && mSimulateSlowMeasure) {
+            simulateExtraMeasureDelay();
+        }
         Trace.endSection();
     }
 
+    private void simulateExtraMeasureDelay() {
+        // Add extra delay in a notification row instead of NotificationStackScrollLayout
+        // to make sure that when the measure cache is used we won't add this delay
+        try {
+            Trace.beginSection("ExtraDebugMeasureDelay");
+            Thread.sleep(SLOW_MEASURE_SIMULATE_DELAY_MS);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
     /**
      * Generates and appends "(MessagingStyle)" type tag to passed string for tracing.
      */
@@ -3414,8 +3437,7 @@
 
     @Override
     protected boolean childNeedsClipping(View child) {
-        if (child instanceof NotificationContentView) {
-            NotificationContentView contentView = (NotificationContentView) child;
+        if (child instanceof NotificationContentView contentView) {
             if (isClippingNeeded()) {
                 return true;
             } else if (hasRoundedCorner()
@@ -3497,8 +3519,7 @@
 
         @Override
         public void applyToView(View view) {
-            if (view instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (view instanceof ExpandableNotificationRow row) {
                 if (row.isExpandAnimationRunning()) {
                     return;
                 }
@@ -3518,8 +3539,7 @@
         @Override
         protected void onYTranslationAnimationFinished(View view) {
             super.onYTranslationAnimationFinished(view);
-            if (view instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (view instanceof ExpandableNotificationRow row) {
                 if (row.isHeadsUpAnimatingAway()) {
                     row.setHeadsUpAnimatingAway(false);
                 }
@@ -3528,8 +3548,7 @@
 
         @Override
         public void animateTo(View child, AnimationProperties properties) {
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow row) {
                 if (row.isExpandAnimationRunning()) {
                     return;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 2a3e69b..aefd348 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,10 +28,9 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
-import com.android.systemui.flags.Flags;
-import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 import com.android.systemui.util.DumpUtilsKt;
 
@@ -49,8 +48,6 @@
     private float mOutlineAlpha = -1f;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    protected final RefactorFlag mImprovedHunAnimation =
-            RefactorFlag.forView(Flags.IMPROVED_HUN_ANIMATIONS);
 
     /**
      * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
@@ -126,7 +123,7 @@
             return EMPTY_PATH;
         }
         float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
-        if (!mImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
+        if (!NotificationsImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
             float overShoot = topRadius + bottomRadius - height;
             float currentTopRoundness = getTopRoundness();
             float currentBottomRoundness = getBottomRoundness();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 49674d6..c4d266e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -676,8 +676,7 @@
         mViewState.headsUpIsVisible = false;
 
         // handling reset for child notifications
-        if (this instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) this;
+        if (this instanceof ExpandableNotificationRow row) {
             List<ExpandableNotificationRow> children = row.getAttachedChildren();
             if (row.isSummaryWithChildren() && children != null) {
                 for (ExpandableNotificationRow childRow : children) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt
index 24e7f05..9556c2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncHybridViewInflation.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.row.shared
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the async hybrid view inflation flag state. */
@@ -24,6 +25,10 @@
 object AsyncHybridViewInflation {
     const val FLAG_NAME = Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is async hybrid (single-line) view inflation enabled */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt
new file mode 100644
index 0000000..16d35fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsImprovedHunAnimation.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notifications improved hun animation flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationsImprovedHunAnimation {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsImprovedHunAnimation()
+
+    /**
+     * 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 699e140..5ab4d4e 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
@@ -16,60 +16,38 @@
 
 package com.android.systemui.statusbar.notification.shelf.ui.viewbinder
 
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.NotificationShelf
-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.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 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.phone.NotificationIconAreaController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
 object NotificationShelfViewBinder {
-    fun bind(
+    suspend fun bind(
         shelf: NotificationShelf,
         viewModel: NotificationShelfViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
         falsingManager: FalsingManager,
-        iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
+        nicBinder: NotificationIconContainerShelfViewBinder,
         notificationIconAreaController: NotificationIconAreaController,
-        shelfIconViewStore: ShelfNotificationIconViewStore,
-    ) {
+    ): Unit = coroutineScope {
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
         shelf.apply {
             if (NotificationIconContainerRefactor.isEnabled) {
-                NotificationIconContainerViewBinder.bindWhileAttached(
-                    shelfIcons,
-                    viewModel.icons,
-                    configuration,
-                    systemBarUtilsState,
-                    iconViewBindingFailureTracker,
-                    shelfIconViewStore,
-                )
+                launch { nicBinder.bind(shelfIcons) }
             } else {
                 notificationIconAreaController.setShelfIcons(shelfIcons)
             }
-            repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    launch {
-                        viewModel.canModifyColorOfNotifications.collect(
-                            ::setCanModifyColorOfNotifications
-                        )
-                    }
-                    launch { viewModel.isClickable.collect(::setCanInteract) }
-                    registerViewListenersWhileAttached(shelf, viewModel)
-                }
+            launch {
+                viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications)
             }
+            launch { viewModel.isClickable.collect(::setCanInteract) }
+            registerViewListenersWhileAttached(shelf, viewModel)
         }
     }
 
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 64b5b62c..5ca8b53 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,7 +18,6 @@
 
 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
@@ -32,7 +31,6 @@
 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/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 0236fc2..45b9c26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -869,8 +869,7 @@
         Path clipPath = mChildClipPath;
         if (clipPath != null) {
             final float translation;
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow notificationRow) {
                 translation = notificationRow.getTranslation();
             } else {
                 translation = child.getTranslationX();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 3bbdfd1..0f640c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -94,9 +94,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.res.R;
-import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.TouchLogger;
-import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.StatusBarState;
@@ -109,12 +107,12 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
-import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -146,8 +144,6 @@
     private static final String TAG = "StackScroller";
     private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE);
 
-    // Delay in milli-seconds before shade closes for clear all.
-    private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
     private boolean mShadeNeedsToClose = false;
 
     @VisibleForTesting
@@ -319,7 +315,7 @@
         }
     };
     private NotificationStackScrollLogger mLogger;
-    private NotificationsController mNotificationsController;
+    private Runnable mResetUserExpandedStatesRunnable;
     private ActivityStarter mActivityStarter;
     private final int[] mTempInt2 = new int[2];
     private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
@@ -482,7 +478,7 @@
     private final Rect mTmpRect = new Rect();
     private ClearAllListener mClearAllListener;
     private ClearAllAnimationListener mClearAllAnimationListener;
-    private ShadeController mShadeController;
+    private Runnable mClearAllFinishedWhilePanelExpandedRunnable;
     private Consumer<Boolean> mOnStackYChanged;
 
     private Interpolator mHideXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
@@ -997,8 +993,7 @@
         for (int i = 0; i < childCount; i++) {
             View child = getChildAt(i);
             if (child.getVisibility() != View.GONE
-                    && child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                    && child instanceof ExpandableNotificationRow row) {
                 if ((row.isPinned() || row.isHeadsUpAnimatingAway()) && row.getTranslation() < 0
                         && row.getProvider().shouldShowGutsOnSnapOpen()) {
                     top = Math.min(top, row.getTranslationY());
@@ -1133,10 +1128,9 @@
             for (int i = 0; i < n; i++) {
                 View view = getChildAt(i);
                 if (view.getVisibility() == View.GONE
-                        || !(view instanceof ExpandableNotificationRow)) {
+                        || !(view instanceof ExpandableNotificationRow row)) {
                     continue;
                 }
-                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
                 currentIndex++;
                 boolean beforeSpeedBump;
                 if (mHighPriorityBeforeSpeedBump) {
@@ -1773,16 +1767,14 @@
     }
 
     public static boolean isPinnedHeadsUp(View v) {
-        if (v instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (v instanceof ExpandableNotificationRow row) {
             return row.isHeadsUp() && row.isPinned();
         }
         return false;
     }
 
     private boolean isHeadsUp(View v) {
-        if (v instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (v instanceof ExpandableNotificationRow row) {
             return row.isHeadsUp();
         }
         return false;
@@ -1824,8 +1816,7 @@
 
             if ((bottom - top >= mMinInteractionHeight || !requireMinHeight)
                     && touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
-                if (slidingChild instanceof ExpandableNotificationRow) {
-                    ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild;
+                if (slidingChild instanceof ExpandableNotificationRow row) {
                     NotificationEntry entry = row.getEntry();
                     if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
                             && mTopHeadsUpEntry.getRow() != row
@@ -2368,8 +2359,7 @@
             float rowTranslation = child.getTranslationY();
             if (rowTranslation >= translationY) {
                 return child;
-            } else if (!ignoreChildren && child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            } else if (!ignoreChildren && child instanceof ExpandableNotificationRow row) {
                 if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
                     List<ExpandableNotificationRow> notificationChildren =
                             row.getAttachedChildren();
@@ -2890,8 +2880,7 @@
     }
 
     private void focusNextViewIfFocused(View view) {
-        if (view instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+        if (view instanceof ExpandableNotificationRow row) {
             if (row.shouldRefocusOnDismiss()) {
                 View nextView = row.getChildAfterViewWhenDismissed();
                 if (nextView == null) {
@@ -3039,8 +3028,7 @@
     }
 
     private int getIntrinsicHeight(View view) {
-        if (view instanceof ExpandableView) {
-            ExpandableView expandableView = (ExpandableView) view;
+        if (view instanceof ExpandableView expandableView) {
             return expandableView.getIntrinsicHeight();
         }
         return view.getHeight();
@@ -3130,8 +3118,7 @@
         generateAddAnimation(child, false /* fromMoreCard */);
         updateAnimationState(child);
         updateChronometerForChild(child);
-        if (child instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+        if (child instanceof ExpandableNotificationRow row) {
             row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX);
 
         }
@@ -3200,8 +3187,7 @@
     }
 
     private void updateAnimationState(boolean running, View child) {
-        if (child instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+        if (child instanceof ExpandableNotificationRow row) {
             row.setAnimationRunning(running);
         }
     }
@@ -3328,8 +3314,10 @@
                     logHunAnimationSkipped(row, "row has no viewState");
                     continue;
                 }
+                boolean shouldHunAppearFromTheBottom =
+                        mStackScrollAlgorithm.shouldHunAppearFromBottom(mAmbientState, viewState);
                 if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) {
-                    if (pinnedAndClosed || shouldHunAppearFromBottom(viewState)) {
+                    if (pinnedAndClosed || shouldHunAppearFromTheBottom) {
                         // Our custom add animation
                         type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
                     } else {
@@ -3341,6 +3329,11 @@
             }
             AnimationEvent event = new AnimationEvent(row, type);
             event.headsUpFromBottom = onBottom;
+            if (NotificationsImprovedHunAnimation.isEnabled()) {
+                // TODO(b/283084712) remove this with the flag and update the HUN filters at
+                //  creation
+                event.filter.animateHeight = false;
+            }
             mAnimationEvents.add(event);
             if (SPEW) {
                 Log.v(TAG, "Generating HUN animation event: "
@@ -3355,11 +3348,6 @@
         mAddedHeadsUpChildren.clear();
     }
 
-    private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) {
-        return viewState.getYTranslation() + viewState.height
-                >= mAmbientState.getMaxHeadsUpTranslation();
-    }
-
     private void generateGroupExpansionEvent() {
         // Generate a group expansion/collapsing event if there is such a group at all
         if (mExpandedGroupView != null) {
@@ -3396,8 +3384,7 @@
             // we need to know the view after this one
             float removedTranslation = child.getTranslationY();
             boolean ignoreChildren = true;
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow row) {
                 if (row.isRemoved() && row.wasChildInGroupWhenRemoved()) {
                     removedTranslation = row.getTranslationWhenRemoved();
                     ignoreChildren = false;
@@ -3439,8 +3426,7 @@
     private void generatePositionChangeEvents() {
         for (ExpandableView child : mChildrenChangingPositions) {
             Integer duration = null;
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow row) {
                 if (row.getEntry().isMarkedForUserTriggeredMovement()) {
                     duration = StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE;
                     row.getEntry().markForUserTriggeredMovement(false);
@@ -4114,7 +4100,7 @@
         mAmbientState.setExpansionChanging(false);
         if (!mIsExpanded) {
             resetScrollPosition();
-            mNotificationsController.resetUserExpandedStates();
+            mResetUserExpandedStatesRunnable.run();
             clearTemporaryViews();
             clearUserLockedViews();
             resetAllSwipeState();
@@ -4124,8 +4110,7 @@
     private void clearUserLockedViews() {
         for (int i = 0; i < getChildCount(); i++) {
             ExpandableView child = getChildAtIndex(i);
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow row) {
                 row.setUserLocked(false);
             }
         }
@@ -4139,8 +4124,7 @@
         );
         for (int i = 0; i < getChildCount(); i++) {
             ExpandableView child = getChildAtIndex(i);
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow row) {
                 clearTemporaryViewsInGroup(
                         /* viewGroup = */ row.getChildrenContainer(),
                         /* reason = */ "clearTemporaryViewsInGroup(row.getChildrenContainer())"
@@ -4225,8 +4209,7 @@
     }
 
     void updateChronometerForChild(View child) {
-        if (child instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+        if (child instanceof ExpandableNotificationRow row) {
             row.setChronometerRunning(mIsExpanded);
         }
     }
@@ -4265,8 +4248,7 @@
     }
 
     private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
-        if (view instanceof ExpandableNotificationRow && !onKeyguard()) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+        if (view instanceof ExpandableNotificationRow row && !onKeyguard()) {
             // TODO: once we're recycling this will need to check the adapter position of the child
             if (row.isUserLocked() && row != getFirstChildNotGone()) {
                 if (row.isSummaryWithChildren()) {
@@ -4316,26 +4298,16 @@
             if (mShadeNeedsToClose) {
                 mShadeNeedsToClose = false;
                 if (mIsExpanded) {
-                    collapseShadeDelayed();
+                    mClearAllFinishedWhilePanelExpandedRunnable.run();
                 }
             }
         }
     }
 
-    private void collapseShadeDelayed() {
-        postDelayed(
-                () -> {
-                    mShadeController.animateCollapseShade(
-                            CommandQueue.FLAG_EXCLUDE_NONE);
-                },
-                DELAY_BEFORE_SHADE_CLOSE /* delayMillis */);
-    }
-
     private void clearHeadsUpDisappearRunning() {
         for (int i = 0; i < getChildCount(); i++) {
             View view = getChildAt(i);
-            if (view instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (view instanceof ExpandableNotificationRow row) {
                 row.setHeadsUpAnimatingAway(false);
                 if (row.isSummaryWithChildren()) {
                     for (ExpandableNotificationRow child : row.getAttachedChildren()) {
@@ -4747,8 +4719,8 @@
         return max + getStackTranslation();
     }
 
-    public void setNotificationsController(NotificationsController notificationsController) {
-        this.mNotificationsController = notificationsController;
+    public void setResetUserExpandedStatesRunnable(Runnable runnable) {
+        this.mResetUserExpandedStatesRunnable = runnable;
     }
 
     public void setActivityStarter(ActivityStarter activityStarter) {
@@ -4932,7 +4904,9 @@
      */
     public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
         mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
+        mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height);
         mStateAnimator.setHeadsUpAppearHeightBottom(height);
+        mStateAnimator.setStackTopMargin(mAmbientState.getStackTopMargin());
         requestChildrenUpdate();
     }
 
@@ -5217,8 +5191,7 @@
                     }
                     View swipedView = mSwipeHelper.getSwipedView();
                     pw.println("Swiped view: " + swipedView);
-                    if (swipedView instanceof ExpandableView) {
-                        ExpandableView expandableView = (ExpandableView) swipedView;
+                    if (swipedView instanceof ExpandableView expandableView) {
                         expandableView.dump(pw, args);
                     }
                 });
@@ -5301,8 +5274,7 @@
         if (view instanceof SectionHeaderView && silentSectionWillBeGone) {
             return true;
         }
-        if (view instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+        if (view instanceof ExpandableNotificationRow row) {
             if (isVisible(row) && includeChildInClearAll(row, selection)) {
                 return true;
             }
@@ -5328,9 +5300,7 @@
             if (shouldHideParent(view, selection)) {
                 viewsToHide.add(view);
             }
-            if (view instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
-
+            if (view instanceof ExpandableNotificationRow parent) {
                 if (isChildrenVisible(parent)) {
                     for (ExpandableNotificationRow child : parent.getAttachedChildren()) {
                         if (isVisible(child) && includeChildInClearAll(child, selection)) {
@@ -5350,10 +5320,9 @@
 
         for (int i = 0; i < childCount; i++) {
             final View view = getChildAt(i);
-            if (!(view instanceof ExpandableNotificationRow)) {
+            if (!(view instanceof ExpandableNotificationRow parent)) {
                 continue;
             }
-            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
             if (includeChildInClearAll(parent, selection)) {
                 viewsToRemove.add(parent);
             }
@@ -5681,8 +5650,8 @@
         mFooterClearAllListener = listener;
     }
 
-    void setShadeController(ShadeController shadeController) {
-        mShadeController = shadeController;
+    void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) {
+        mClearAllFinishedWhilePanelExpandedRunnable = runnable;
     }
 
     /**
@@ -5992,8 +5961,7 @@
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
             mSwipeHelper.forceResetSwipeState(child);
-            if (child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow childRow = (ExpandableNotificationRow) child;
+            if (child instanceof ExpandableNotificationRow childRow) {
                 List<ExpandableNotificationRow> grandchildren = childRow.getAttachedChildren();
                 if (grandchildren != null) {
                     for (ExpandableNotificationRow grandchild : grandchildren) {
@@ -6279,8 +6247,7 @@
     }
 
     static boolean canChildBeDismissed(View v) {
-        if (v instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (v instanceof ExpandableNotificationRow row) {
             if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) {
                 return false;
             }
@@ -6290,8 +6257,7 @@
     }
 
     static boolean canChildBeCleared(View v) {
-        if (v instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+        if (v instanceof ExpandableNotificationRow row) {
             if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) {
                 return false;
             }
@@ -6386,8 +6352,7 @@
         /* Only ever called as a consequence of an expansion gesture in the shade. */
         @Override
         public void setUserExpandedChild(View v, boolean userExpanded) {
-            if (v instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            if (v instanceof ExpandableNotificationRow row) {
                 if (userExpanded && onKeyguard()) {
                     // Due to a race when locking the screen while touching, a notification may be
                     // expanded even after we went back to keyguard. An example of this happens if
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 e6315fd..6f5058c 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
@@ -81,6 +81,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeViewController;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
@@ -152,6 +153,8 @@
     private static final String TAG = "StackScrollerController";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
     private static final String HIGH_PRIORITY = "high_priority";
+    /** Delay in milli-seconds before shade closes for clear all. */
+    private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
 
     private final boolean mAllowLongPress;
     private final NotificationGutsManager mNotificationGutsManager;
@@ -402,8 +405,7 @@
             if (!mAllowLongPress) {
                 return;
             }
-            if (view instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (view instanceof ExpandableNotificationRow row) {
                 mMetricsLogger.write(row.getEntry().getSbn().getLogMaker()
                         .setCategory(MetricsEvent.ACTION_TOUCH_GEAR)
                         .setType(MetricsEvent.TYPE_ACTION)
@@ -423,8 +425,7 @@
 
         @Override
         public void onMenuShown(View row) {
-            if (row instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) row;
+            if (row instanceof ExpandableNotificationRow notificationRow) {
                 mMetricsLogger.write(notificationRow.getEntry().getSbn().getLogMaker()
                         .setCategory(MetricsEvent.ACTION_REVEAL_GEAR)
                         .setType(MetricsEvent.TYPE_ACTION));
@@ -489,10 +490,9 @@
                  */
                 @Override
                 public void onChildDismissed(View view) {
-                    if (!(view instanceof ActivatableNotificationView)) {
+                    if (!(view instanceof ActivatableNotificationView row)) {
                         return;
                     }
-                    ActivatableNotificationView row = (ActivatableNotificationView) view;
                     if (!row.isDismissed()) {
                         handleChildViewDismissed(view);
                     }
@@ -516,8 +516,7 @@
                     if (mView.getClearAllInProgress()) {
                         return;
                     }
-                    if (view instanceof ExpandableNotificationRow) {
-                        ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                    if (view instanceof ExpandableNotificationRow row) {
                         if (row.isHeadsUp()) {
                             mHeadsUpManager.addSwipedOutNotification(
                                     row.getEntry().getSbn().getKey());
@@ -548,8 +547,7 @@
                             ev.getY(),
                             true /* requireMinHeight */,
                             false /* ignoreDecors */);
-                    if (child instanceof ExpandableNotificationRow) {
-                        ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                    if (child instanceof ExpandableNotificationRow row) {
                         ExpandableNotificationRow parent = row.getNotificationParent();
                         if (parent != null && parent.areChildrenExpanded()
                                 && (parent.areGutsExposed()
@@ -579,8 +577,7 @@
                 @Override
                 public void onChildSnappedBack(View animView, float targetLeft) {
                     mView.onSwipeEnd();
-                    if (animView instanceof ExpandableNotificationRow) {
-                        ExpandableNotificationRow row = (ExpandableNotificationRow) animView;
+                    if (animView instanceof ExpandableNotificationRow row) {
                         if (row.isPinned() && !canChildBeDismissed(row)
                                 && row.getEntry().getSbn().getNotification().fullScreenIntent
                                 == null) {
@@ -754,7 +751,7 @@
         mView.setController(this);
         mView.setLogger(mLogger);
         mView.setTouchHandler(new TouchHandler());
-        mView.setNotificationsController(mNotificationsController);
+        mView.setResetUserExpandedStatesRunnable(mNotificationsController::resetUserExpandedStates);
         mView.setActivityStarter(mActivityStarter);
         mView.setClearAllAnimationListener(this::onAnimationEnd);
         mView.setClearAllListener((selection) -> mUiEventLogger.log(
@@ -770,7 +767,11 @@
                 mView.setIsRemoteInputActive(active);
             }
         });
-        mView.setShadeController(mShadeController);
+        mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> {
+            final Runnable doCollapseRunnable = () ->
+                    mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE);
+            mView.postDelayed(doCollapseRunnable, /* delayMillis = */ DELAY_BEFORE_SHADE_CLOSE);
+        });
         mDumpManager.registerDumpable(mView);
 
         mKeyguardBypassController.registerOnBypassStateChangedListener(
@@ -852,7 +853,7 @@
         mGroupExpansionManager.registerGroupExpansionChangeListener(
                 (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
 
-        mViewBinder.bind(mView, this);
+        mViewBinder.bindWhileAttached(mView, this);
 
         if (!FooterViewRefactor.isEnabled()) {
             collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
@@ -1973,8 +1974,7 @@
 
             // Check if we need to clear any snooze leavebehinds
             if (guts != null && !NotificationSwipeHelper.isTouchInView(ev, guts)
-                    && guts.getGutsContent() instanceof NotificationSnooze) {
-                NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent();
+                    && guts.getGutsContent() instanceof NotificationSnooze ns) {
                 if ((ns.isExpanded() && isCancelOrUp)
                         || (!horizontalSwipeWantsIt && scrollerWantsIt)) {
                     // If the leavebehind is expanded we clear it on the next up event, otherwise we
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 06ca9a5..664a6b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -66,12 +67,15 @@
     private boolean mClipNotificationScrollToTop;
     @VisibleForTesting
     float mHeadsUpInset;
+    @VisibleForTesting
+    float mHeadsUpAppearStartAboveScreen;
     private int mPinnedZTranslationExtra;
     private float mNotificationScrimPadding;
     private int mMarginBottom;
     private float mQuickQsOffsetHeight;
     private float mSmallCornerRadius;
     private float mLargeCornerRadius;
+    private int mHeadsUpAppearHeightBottom;
 
     public StackScrollAlgorithm(
             Context context,
@@ -94,6 +98,8 @@
         int statusBarHeight = SystemBarUtils.getStatusBarHeight(context);
         mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
                 R.dimen.heads_up_status_bar_padding);
+        mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize(
+                R.dimen.heads_up_appear_y_above_screen);
         mPinnedZTranslationExtra = res.getDimensionPixelSize(
                 R.dimen.heads_up_pinned_elevation);
         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
@@ -221,6 +227,25 @@
         return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState);
     }
 
+    public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
+        mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
+    }
+
+    /**
+     * If the QuickSettings is showing full screen, we want to animate the HeadsUp Notifications
+     * from the bottom of the screen.
+     *
+     * @param ambientState Current ambient state.
+     * @param viewState The state of the HUN that is being queried to appear from the bottom.
+     *
+     * @return true if the HeadsUp Notifications should appear from the bottom
+     */
+    public boolean shouldHunAppearFromBottom(AmbientState ambientState,
+            ExpandableViewState viewState) {
+        return viewState.getYTranslation() + viewState.height
+                >= ambientState.getMaxHeadsUpTranslation();
+    }
+
     public static void log(String s) {
         if (DEBUG) {
             android.util.Log.i(TAG, s);
@@ -229,8 +254,7 @@
 
     public static void logView(View view, String s) {
         String viewString = "";
-        if (view instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = ((ExpandableNotificationRow) view);
+        if (view instanceof ExpandableNotificationRow row) {
             if (row.getEntry() == null) {
                 viewString = "ExpandableNotificationRow has null NotificationEntry";
             } else {
@@ -264,8 +288,7 @@
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             ExpandableView v = algorithmState.visibleChildren.get(i);
-            if (v instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+            if (v instanceof ExpandableNotificationRow row) {
                 row.updateChildrenStates();
             }
         }
@@ -376,8 +399,7 @@
                     continue;
                 }
                 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
-                if (v instanceof ExpandableNotificationRow) {
-                    ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+                if (v instanceof ExpandableNotificationRow row) {
 
                     // handle the notGoneIndex for the children as well
                     List<ExpandableNotificationRow> children = row.getAttachedChildren();
@@ -508,10 +530,9 @@
     private boolean hasNonClearableNotifs(StackScrollAlgorithmState algorithmState) {
         for (int i = 0; i < algorithmState.visibleChildren.size(); i++) {
             View child = algorithmState.visibleChildren.get(i);
-            if (!(child instanceof ExpandableNotificationRow)) {
+            if (!(child instanceof ExpandableNotificationRow row)) {
                 continue;
             }
-            final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
             if (!row.canViewBeCleared()) {
                 return true;
             }
@@ -715,10 +736,9 @@
         ExpandableNotificationRow pulsingRow = null;
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
-            if (!(child instanceof ExpandableNotificationRow)) {
+            if (!(child instanceof ExpandableNotificationRow row)) {
                 continue;
             }
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
             if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) {
                 continue;
             }
@@ -760,10 +780,9 @@
         ExpandableNotificationRow topHeadsUpEntry = null;
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
-            if (!(child instanceof ExpandableNotificationRow)) {
+            if (!(child instanceof ExpandableNotificationRow row)) {
                 continue;
             }
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
             if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) {
                 continue;
             }
@@ -793,10 +812,16 @@
                 }
             }
             if (row.isPinned()) {
-                // Make sure row yTranslation is at maximum the HUN yTranslation,
-                // which accounts for AmbientState.stackTopMargin in split-shade.
-                childState.setYTranslation(
-                        Math.max(childState.getYTranslation(), headsUpTranslation));
+                if (NotificationsImprovedHunAnimation.isEnabled()) {
+                    // Make sure row yTranslation is at the HUN yTranslation,
+                    // which accounts for AmbientState.stackTopMargin in split-shade.
+                    childState.setYTranslation(headsUpTranslation);
+                } else {
+                    // Make sure row yTranslation is at maximum the HUN yTranslation,
+                    // which accounts for AmbientState.stackTopMargin in split-shade.
+                    childState.setYTranslation(
+                            Math.max(childState.getYTranslation(), headsUpTranslation));
+                }
                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
                 childState.hidden = false;
                 ExpandableViewState topState =
@@ -819,10 +844,22 @@
                 }
             }
             if (row.isHeadsUpAnimatingAway()) {
-                // Make sure row yTranslation is at maximum the HUN yTranslation,
-                // which accounts for AmbientState.stackTopMargin in split-shade.
-                childState.setYTranslation(
-                        Math.max(childState.getYTranslation(), headsUpTranslation));
+                if (NotificationsImprovedHunAnimation.isEnabled()) {
+                    if (shouldHunAppearFromBottom(ambientState, childState)) {
+                        // move to the bottom of the screen
+                        childState.setYTranslation(
+                                mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen);
+                    } else {
+                        // move to the top of the screen
+                        childState.setYTranslation(-ambientState.getStackTopMargin()
+                                - mHeadsUpAppearStartAboveScreen);
+                    }
+                } else {
+                    // Make sure row yTranslation is at maximum the HUN yTranslation,
+                    // which accounts for AmbientState.stackTopMargin in split-shade.
+                    childState.setYTranslation(
+                            Math.max(childState.getYTranslation(), headsUpTranslation));
+                }
                 // keep it visible for the animation
                 childState.hidden = false;
             }
@@ -897,8 +934,7 @@
     }
 
     protected int getMaxAllowedChildHeight(View child) {
-        if (child instanceof ExpandableView) {
-            ExpandableView expandableView = (ExpandableView) child;
+        if (child instanceof ExpandableView expandableView) {
             return expandableView.getIntrinsicHeight();
         }
         return child == null ? mCollapsedSize : child.getHeight();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index e94258f..a3e0941 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK;
 
@@ -26,6 +27,7 @@
 import android.view.View;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardSliceView;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -33,6 +35,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -68,6 +71,8 @@
 
     private final int mGoToFullShadeAppearingTranslation;
     private final int mPulsingAppearingTranslation;
+    @VisibleForTesting
+    float mHeadsUpAppearStartAboveScreen;
     private final ExpandableViewState mTmpState = new ExpandableViewState();
     private final AnimationProperties mAnimationProperties;
     public NotificationStackScrollLayout mHostLayout;
@@ -85,21 +90,23 @@
     private ValueAnimator mTopOverScrollAnimator;
     private ValueAnimator mBottomOverScrollAnimator;
     private int mHeadsUpAppearHeightBottom;
+    private int mStackTopMargin;
     private boolean mShadeExpanded;
     private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>();
     private NotificationShelf mShelf;
-    private float mStatusBarIconLocation;
-    private int[] mTmpLocation = new int[2];
     private StackStateLogger mLogger;
 
     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
         mHostLayout = hostLayout;
+        // TODO(b/317061579) reload on configuration changes
         mGoToFullShadeAppearingTranslation =
                 hostLayout.getContext().getResources().getDimensionPixelSize(
                         R.dimen.go_to_full_shade_appearing_translation);
         mPulsingAppearingTranslation =
                 hostLayout.getContext().getResources().getDimensionPixelSize(
                         R.dimen.pulsing_notification_appear_translation);
+        mHeadsUpAppearStartAboveScreen = hostLayout.getContext().getResources()
+                .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen);
         mAnimationProperties = new AnimationProperties() {
             @Override
             public AnimationFilter getAnimationFilter() {
@@ -455,8 +462,37 @@
                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView;
                 row.prepareExpansionChanged();
-            } else if (event.animationType == NotificationStackScrollLayout
-                    .AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
+            } else if (NotificationsImprovedHunAnimation.isEnabled()
+                    && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) {
+                mHeadsUpAppearChildren.add(changingView);
+
+                mTmpState.copyFrom(changingView.getViewState());
+                if (event.headsUpFromBottom) {
+                    // start from the bottom of the screen
+                    mTmpState.setYTranslation(
+                            mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen);
+                } else {
+                    // start from the top of the screen
+                    mTmpState.setYTranslation(
+                            -mStackTopMargin - mHeadsUpAppearStartAboveScreen);
+                }
+                // set the height and the initial position
+                mTmpState.applyToView(changingView);
+                mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
+                        Interpolators.FAST_OUT_SLOW_IN);
+
+                Runnable onAnimationEnd = null;
+                if (loggable) {
+                    // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with
+                    // normal ADD animations, which would not be logged here.
+                    String finalKey = key;
+                    mLogger.logHUNViewAppearing(key);
+                    onAnimationEnd = () -> mLogger.appearAnimationEnded(finalKey);
+                }
+                changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR,
+                        /* isHeadsUpAppear= */ true, onAnimationEnd);
+            } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) {
+                NotificationsImprovedHunAnimation.assertInLegacyMode();
                 // This item is added, initialize its properties.
                 ExpandableViewState viewState = changingView.getViewState();
                 mTmpState.copyFrom(viewState);
@@ -536,6 +572,10 @@
                             changingView.setInRemovalAnimation(true);
                         };
                     }
+                    if (NotificationsImprovedHunAnimation.isEnabled()) {
+                        mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
+                                Interpolators.FAST_OUT_SLOW_IN_REVERSE);
+                    }
                     long removeAnimationDelay = changingView.performRemoveAnimation(
                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
                             0, 0.0f, true /* isHeadsUpAppear */,
@@ -601,6 +641,10 @@
         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
     }
 
+    public void setStackTopMargin(int stackTopMargin) {
+        mStackTopMargin = stackTopMargin;
+    }
+
     public void setShadeExpanded(boolean shadeExpanded) {
         mShadeExpanded = shadeExpanded;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
index 4de3a7f..3a650aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
@@ -18,7 +18,7 @@
 import android.graphics.Rect
 import android.util.Log
 import com.android.app.tracing.FlowTracing.traceEach
-import com.android.systemui.common.domain.interactor.ConfigurationInteractor
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
@@ -28,6 +28,7 @@
 import com.android.systemui.util.kotlin.area
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.race
+import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.flow.Flow
@@ -38,7 +39,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.withTimeout
-import javax.inject.Inject
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
index 98c1734..3cd9a8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.stack.shared
 
 import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the DisplaySwitchNotificationsHider flag state. */
@@ -24,6 +25,10 @@
 object DisplaySwitchNotificationsHiderFlag {
     const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH
 
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
     /** Is the hiding enabled? */
     @JvmStatic
     inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
index 274bf94..910b40f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -16,29 +16,22 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import androidx.core.view.doOnDetach
-import androidx.lifecycle.lifecycleScope
-import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
-import kotlinx.coroutines.launch
 
 /**
  * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
  */
 object HideNotificationsBinder {
-    fun bindHideList(
+    suspend fun bindHideList(
         viewController: NotificationStackScrollLayoutController,
         viewModel: NotificationListViewModel
     ) {
-        viewController.view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
-                    viewController.bindHideState(shouldHide)
-                }
-            }
-        }
-
         viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
+
+        viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
+            viewController.bindHideState(shouldHide)
+        }
     }
 
     private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
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 9373d49..1b36660 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
@@ -32,15 +32,14 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 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.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 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.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import com.android.systemui.util.kotlin.getOrNull
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.combine
@@ -55,25 +54,27 @@
     private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
     private val iconAreaController: NotificationIconAreaController,
-    private val iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val metricsLogger: MetricsLogger,
-    private val shelfIconViewStore: ShelfNotificationIconViewStore,
-    private val systemBarUtilsState: SystemBarUtilsState,
+    private val nicBinder: NotificationIconContainerShelfViewBinder,
 ) {
 
-    fun bind(
+    fun bindWhileAttached(
         view: NotificationStackScrollLayout,
         viewController: NotificationStackScrollLayoutController
     ) {
-        bindShelf(view)
-        bindHideList(viewController, viewModel)
+        val shelf =
+            LayoutInflater.from(view.context)
+                .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf
+        view.setShelf(shelf)
 
-        if (FooterViewRefactor.isEnabled) {
-            bindFooter(view)
-            bindEmptyShade(view)
+        view.repeatWhenAttached {
+            lifecycleScope.launch {
+                launch { bindShelf(shelf) }
+                launch { bindHideList(viewController, viewModel) }
 
-            view.repeatWhenAttached {
-                lifecycleScope.launch {
+                if (FooterViewRefactor.isEnabled) {
+                    launch { bindFooter(view) }
+                    launch { bindEmptyShade(view) }
                     viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
                         view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
                     }
@@ -82,73 +83,57 @@
         }
     }
 
-    private fun bindShelf(parentView: NotificationStackScrollLayout) {
-        val shelf =
-            LayoutInflater.from(parentView.context)
-                .inflate(R.layout.status_bar_notification_shelf, parentView, false)
-                as NotificationShelf
+    private suspend fun bindShelf(shelf: NotificationShelf) {
         NotificationShelfViewBinder.bind(
             shelf,
             viewModel.shelf,
-            configuration,
-            systemBarUtilsState,
             falsingManager,
-            iconViewBindingFailureTracker,
+            nicBinder,
             iconAreaController,
-            shelfIconViewStore,
         )
-        parentView.setShelf(shelf)
     }
 
-    private fun bindFooter(parentView: NotificationStackScrollLayout) {
-        viewModel.footer.ifPresent { footerViewModel ->
+    private suspend fun bindFooter(parentView: NotificationStackScrollLayout) {
+        viewModel.footer.getOrNull()?.let { footerViewModel ->
             // The footer needs to be re-inflated every time the theme or the font size changes.
-            parentView.repeatWhenAttached {
-                configuration.reinflateAndBindLatest(
-                    R.layout.status_bar_notification_footer,
-                    parentView,
-                    attachToRoot = false,
-                    backgroundDispatcher,
-                ) { footerView: FooterView ->
-                    traceSection("bind FooterView") {
-                        val disposableHandle =
-                            FooterViewBinder.bind(
-                                footerView,
-                                footerViewModel,
-                                clearAllNotifications = {
-                                    metricsLogger.action(
-                                        MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
-                                    )
-                                    parentView.clearAllNotifications()
-                                },
-                            )
-                        parentView.setFooterView(footerView)
-                        return@reinflateAndBindLatest disposableHandle
-                    }
+            configuration.reinflateAndBindLatest(
+                R.layout.status_bar_notification_footer,
+                parentView,
+                attachToRoot = false,
+                backgroundDispatcher,
+            ) { footerView: FooterView ->
+                traceSection("bind FooterView") {
+                    val disposableHandle =
+                        FooterViewBinder.bind(
+                            footerView,
+                            footerViewModel,
+                            clearAllNotifications = {
+                                metricsLogger.action(
+                                    MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
+                                )
+                                parentView.clearAllNotifications()
+                            },
+                        )
+                    parentView.setFooterView(footerView)
+                    return@reinflateAndBindLatest disposableHandle
                 }
             }
         }
     }
 
-    private fun bindEmptyShade(
-        parentView: NotificationStackScrollLayout,
-    ) {
-        parentView.repeatWhenAttached {
-            lifecycleScope.launch {
-                combine(
-                        viewModel.shouldShowEmptyShadeView,
-                        viewModel.areNotificationsHiddenInShade,
-                        viewModel.hasFilteredOutSeenNotifications,
-                        ::Triple
-                    )
-                    .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
-                        parentView.updateEmptyShadeView(
-                            shouldShow,
-                            areNotifsHidden,
-                            hasFilteredNotifs,
-                        )
-                    }
+    private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+        combine(
+                viewModel.shouldShowEmptyShadeView,
+                viewModel.areNotificationsHiddenInShade,
+                viewModel.hasFilteredOutSeenNotifications,
+                ::Triple
+            )
+            .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
+                parentView.updateEmptyShadeView(
+                    shouldShow,
+                    areNotifsHidden,
+                    hasFilteredNotifs,
+                )
             }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index 97cb45a..6e8ad2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -21,13 +21,17 @@
 import android.os.LocaleList
 import android.view.View.LAYOUT_DIRECTION_RTL
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import javax.inject.Inject
 
 @SysUISingleton
-class ConfigurationControllerImpl @Inject constructor(context: Context) : ConfigurationController {
+class ConfigurationControllerImpl @Inject constructor(
+        @Application context: Context,
+        ) : ConfigurationController {
 
-    private val listeners: MutableList<ConfigurationController.ConfigurationListener> = ArrayList()
+    private val listeners: MutableList<ConfigurationListener> = ArrayList()
     private val lastConfig = Configuration()
     private var density: Int = 0
     private var smallestScreenWidth: Int = 0
@@ -143,14 +147,12 @@
         }
     }
 
-
-
-    override fun addCallback(listener: ConfigurationController.ConfigurationListener) {
+    override fun addCallback(listener: ConfigurationListener) {
         listeners.add(listener)
         listener.onDensityOrFontScaleChanged()
     }
 
-    override fun removeCallback(listener: ConfigurationController.ConfigurationListener) {
+    override fun removeCallback(listener: ConfigurationListener) {
         listeners.remove(listener)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerStartable.kt
new file mode 100644
index 0000000..90ebaf2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerStartable.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.statusbar.phone
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import javax.inject.Inject
+
+@SysUISingleton
+class ConfigurationControllerStartable
+@Inject
+constructor(
+    private val configurationController: ConfigurationController,
+    private val listeners: Set<@JvmSuppressWildcards ConfigurationListener>
+) : CoreStartable {
+    override fun start() {
+        listeners.forEach { configurationController.addCallback(it) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 145dbff..7cc0888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -196,7 +196,7 @@
     }
 
     private void updateKeyguardStatusBarHeight() {
-        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
+        ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
         lp.height = getStatusBarHeaderHeightKeyguard(mContext);
         setLayoutParams(lp);
     }
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 a62a1ed..e79f3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -608,7 +608,7 @@
     }
 
     @Override
-    public void onPulseExpansionChanged(boolean expandingChanged) {
+    public void onPulseExpansionAmountChanged(boolean expandingChanged) {
         if (expandingChanged) {
             updateAodIconsVisibility(true /* animate */, false /* force */);
         }
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 0dabafb..f34a44a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -284,11 +284,22 @@
 
     @Override
     public String toString() {
-        return "NotificationIconContainer("
-                + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen
-                + " overrideIconColor=" + mOverrideIconColor
-                + " speedBumpIndex=" + mSpeedBumpIndex
-                + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + ')';
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            return super.toString()
+                    + " {"
+                    + " overrideIconColor=" + mOverrideIconColor
+                    + ", maxIcons=" + mMaxIcons
+                    + ", isStaticLayout=" + mIsStaticLayout
+                    + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary)
+                    + " }";
+        } else {
+            return "NotificationIconContainer("
+                    + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen
+                    + " overrideIconColor=" + mOverrideIconColor
+                    + " speedBumpIndex=" + mSpeedBumpIndex
+                    + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary)
+                    + ')';
+        }
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
deleted file mode 100644
index 7048a78..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone;
-
-import android.annotation.IntDef;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.drawable.Icon;
-import android.os.UserHandle;
-
-import com.android.internal.statusbar.StatusBarIcon;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
-import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Wraps {@link com.android.internal.statusbar.StatusBarIcon} so we can still have a uniform list
- */
-public class StatusBarIconHolder {
-    public static final int TYPE_ICON = 0;
-    /**
-     * TODO (b/249790733): address this once the new pipeline is in place
-     * This type exists so that the new pipeline (see {@link MobileIconViewModel}) can be used
-     * to inform the old view system about changes to the data set (the list of mobile icons). The
-     * design of the new pipeline should allow for removal of this icon holder type, and obsolete
-     * the need for this entire class.
-     *
-     * @deprecated This field only exists so the new status bar pipeline can interface with the
-     * view holder system.
-     */
-    @Deprecated
-    public static final int TYPE_MOBILE_NEW = 3;
-
-    /**
-     * TODO (b/238425913): address this once the new pipeline is in place
-     * This type exists so that the new wifi pipeline can be used to inform the old view system
-     * about the existence of the wifi icon. The design of the new pipeline should allow for removal
-     * of this icon holder type, and obsolete the need for this entire class.
-     *
-     * @deprecated This field only exists so the new status bar pipeline can interface with the
-     * view holder system.
-     */
-    @Deprecated
-    public static final int TYPE_WIFI_NEW = 4;
-
-    @IntDef({
-            TYPE_ICON,
-            TYPE_MOBILE_NEW,
-            TYPE_WIFI_NEW
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    @interface IconType {}
-
-    private StatusBarIcon mIcon;
-    private @IconType int mType = TYPE_ICON;
-    private int mTag = 0;
-
-    /** Returns a human-readable string representing the given type. */
-    public static String getTypeString(@IconType int type) {
-        switch(type) {
-            case TYPE_ICON: return "ICON";
-            case TYPE_MOBILE_NEW: return "MOBILE_NEW";
-            case TYPE_WIFI_NEW: return "WIFI_NEW";
-            default: return "UNKNOWN";
-        }
-    }
-
-    private StatusBarIconHolder() {
-    }
-
-    public static StatusBarIconHolder fromIcon(StatusBarIcon icon) {
-        StatusBarIconHolder wrapper = new StatusBarIconHolder();
-        wrapper.mIcon = icon;
-
-        return wrapper;
-    }
-
-    /** Creates a new holder with for the new wifi icon. */
-    public static StatusBarIconHolder forNewWifiIcon() {
-        StatusBarIconHolder holder = new StatusBarIconHolder();
-        holder.mType = TYPE_WIFI_NEW;
-        return holder;
-    }
-
-    /**
-     * ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to
-     * determine icon ordering and building the correct view model
-     */
-    public static StatusBarIconHolder fromSubIdForModernMobileIcon(int subId) {
-        StatusBarIconHolder holder = new StatusBarIconHolder();
-        holder.mType = TYPE_MOBILE_NEW;
-        holder.mTag = subId;
-
-        return holder;
-    }
-
-    /**
-     * Creates a new StatusBarIconHolder from a CallIndicatorIconState.
-     */
-    public static StatusBarIconHolder fromCallIndicatorState(
-            Context context,
-            CallIndicatorIconState state) {
-        StatusBarIconHolder holder = new StatusBarIconHolder();
-        int resId = state.isNoCalling ? state.noCallingResId : state.callStrengthResId;
-        String contentDescription = state.isNoCalling
-                ? state.noCallingDescription : state.callStrengthDescription;
-        holder.mIcon = new StatusBarIcon(UserHandle.SYSTEM, context.getPackageName(),
-                Icon.createWithResource(context, resId), 0, 0, contentDescription);
-        holder.mTag = state.subId;
-        return holder;
-    }
-
-    public @IconType int getType() {
-        return mType;
-    }
-
-    @Nullable
-    public StatusBarIcon getIcon() {
-        return mIcon;
-    }
-
-    public void setIcon(StatusBarIcon icon) {
-        mIcon = icon;
-    }
-
-    public boolean isVisible() {
-        switch (mType) {
-            case TYPE_ICON:
-                return mIcon.visible;
-            case TYPE_MOBILE_NEW:
-            case TYPE_WIFI_NEW:
-                // The new pipeline controls visibilities via the view model and view binder, so
-                // this is effectively an unused return value.
-                return true;
-            default:
-                return true;
-        }
-    }
-
-    public void setVisible(boolean visible) {
-        if (isVisible() == visible) {
-            return;
-        }
-
-        switch (mType) {
-            case TYPE_ICON:
-                mIcon.visible = visible;
-                break;
-
-            case TYPE_MOBILE_NEW:
-            case TYPE_WIFI_NEW:
-                // The new pipeline controls visibilities via the view model and view binder, so
-                // ignore setVisible.
-                break;
-        }
-    }
-
-    public int getTag() {
-        return mTag;
-    }
-
-    @Override
-    public String toString() {
-        return "StatusBarIconHolder(type=" + getTypeString(mType)
-                + " tag=" + getTag()
-                + " visible=" + isVisible() + ")";
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
new file mode 100644
index 0000000..5b55a1e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.phone
+
+import android.annotation.IntDef
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import com.android.internal.statusbar.StatusBarIcon
+import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState
+
+/** Wraps [com.android.internal.statusbar.StatusBarIcon] so we can still have a uniform list */
+class StatusBarIconHolder private constructor() {
+    @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW)
+    @Retention(AnnotationRetention.SOURCE)
+    internal annotation class IconType
+
+    var icon: StatusBarIcon? = null
+
+    @IconType
+    var type = TYPE_ICON
+        private set
+
+    var tag = 0
+        private set
+
+    var isVisible: Boolean
+        get() =
+            when (type) {
+                TYPE_ICON -> icon!!.visible
+
+                // The new pipeline controls visibilities via the view model and
+                // view binder, so
+                // this is effectively an unused return value.
+                TYPE_MOBILE_NEW,
+                TYPE_WIFI_NEW -> true
+                else -> true
+            }
+        set(visible) {
+            if (isVisible == visible) {
+                return
+            }
+            when (type) {
+                TYPE_ICON -> icon!!.visible = visible
+                TYPE_MOBILE_NEW,
+                TYPE_WIFI_NEW -> {}
+            }
+        }
+
+    override fun toString(): String {
+        return ("StatusBarIconHolder(type=${getTypeString(type)}" +
+            " tag=$tag" +
+            " visible=$isVisible)")
+    }
+
+    companion object {
+        const val TYPE_ICON = 0
+
+        /**
+         * TODO (b/249790733): address this once the new pipeline is in place This type exists so
+         * that the new pipeline (see [MobileIconViewModel]) can be used to inform the old view
+         * system about changes to the data set (the list of mobile icons). The design of the new
+         * pipeline should allow for removal of this icon holder type, and obsolete the need for
+         * this entire class.
+         */
+        @Deprecated(
+            """This field only exists so the new status bar pipeline can interface with the
+      view holder system."""
+        )
+        const val TYPE_MOBILE_NEW = 3
+
+        /**
+         * TODO (b/238425913): address this once the new pipeline is in place This type exists so
+         * that the new wifi pipeline can be used to inform the old view system about the existence
+         * of the wifi icon. The design of the new pipeline should allow for removal of this icon
+         * holder type, and obsolete the need for this entire class.
+         */
+        @Deprecated(
+            """This field only exists so the new status bar pipeline can interface with the
+      view holder system."""
+        )
+        const val TYPE_WIFI_NEW = 4
+
+        /** Returns a human-readable string representing the given type. */
+        fun getTypeString(@IconType type: Int): String {
+            return when (type) {
+                TYPE_ICON -> "ICON"
+                TYPE_MOBILE_NEW -> "MOBILE_NEW"
+                TYPE_WIFI_NEW -> "WIFI_NEW"
+                else -> "UNKNOWN"
+            }
+        }
+
+        @JvmStatic
+        fun fromIcon(icon: StatusBarIcon?): StatusBarIconHolder {
+            val wrapper = StatusBarIconHolder()
+            wrapper.icon = icon
+            return wrapper
+        }
+
+        /** Creates a new holder with for the new wifi icon. */
+        @JvmStatic
+        fun forNewWifiIcon(): StatusBarIconHolder {
+            val holder = StatusBarIconHolder()
+            holder.type = TYPE_WIFI_NEW
+            return holder
+        }
+
+        /**
+         * ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to
+         * determine icon ordering and building the correct view model
+         */
+        @JvmStatic
+        fun fromSubIdForModernMobileIcon(subId: Int): StatusBarIconHolder {
+            val holder = StatusBarIconHolder()
+            holder.type = TYPE_MOBILE_NEW
+            holder.tag = subId
+            return holder
+        }
+
+        /** Creates a new StatusBarIconHolder from a CallIndicatorIconState. */
+        @JvmStatic
+        fun fromCallIndicatorState(
+            context: Context,
+            state: CallIndicatorIconState,
+        ): StatusBarIconHolder {
+            val holder = StatusBarIconHolder()
+            val resId = if (state.isNoCalling) state.noCallingResId else state.callStrengthResId
+            val contentDescription =
+                if (state.isNoCalling) state.noCallingDescription else state.callStrengthDescription
+            holder.icon =
+                StatusBarIcon(
+                    UserHandle.SYSTEM,
+                    context.packageName,
+                    Icon.createWithResource(context, resId),
+                    0,
+                    0,
+                    contentDescription,
+                )
+            holder.tag = state.subId
+            return holder
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index 942d186..b048da492 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -16,12 +16,16 @@
 
 package com.android.systemui.statusbar.phone.dagger;
 
+import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
+import com.android.systemui.statusbar.phone.ConfigurationControllerStartable;
 
 import dagger.Binds;
 import dagger.Module;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
 
 /**
  * Dagger Module providing {@link CentralSurfacesImpl}.
@@ -34,4 +38,12 @@
     @Binds
     @SysUISingleton
     CentralSurfaces bindsCentralSurfaces(CentralSurfacesImpl impl);
+
+    /**
+     * Starts {@link ConfigurationControllerStartable}
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(ConfigurationControllerStartable.class)
+    CoreStartable bindConfigControllerStartable(ConfigurationControllerStartable impl);
 }
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 cd99934..11456ff 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
@@ -38,7 +38,6 @@
 import com.android.app.animation.InterpolatorsAndroidX;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
@@ -54,10 +53,7 @@
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-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.viewbinder.NotificationIconContainerStatusBarViewBinder;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
@@ -75,7 +71,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -95,6 +90,8 @@
 
 import kotlin.Unit;
 
+import kotlinx.coroutines.DisposableHandle;
+
 /**
  * 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
@@ -151,10 +148,7 @@
     private final DumpManager mDumpManager;
     private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final NotificationIconContainerStatusBarViewModel mStatusBarIconsViewModel;
-    private final ConfigurationState mConfigurationState;
-    private final SystemBarUtilsState mSystemBarUtilsState;
-    private final StatusBarNotificationIconViewStore mStatusBarIconViewStore;
+    private final NotificationIconContainerStatusBarViewBinder mNicViewBinder;
     private final DemoModeController mDemoModeController;
 
     private List<String> mBlockedIcons = new ArrayList<>();
@@ -216,7 +210,7 @@
         mWaitingForWindowStateChangeAfterCameraLaunch = false;
         mTransitionFromLockscreenToDreamStarted = false;
     };
-    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
+    private DisposableHandle mNicBindingDisposable;
 
     @Inject
     public CollapsedStatusBarFragment(
@@ -234,7 +228,7 @@
             KeyguardStateController keyguardStateController,
             ShadeViewController shadeViewController,
             StatusBarStateController statusBarStateController,
-            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
+            NotificationIconContainerStatusBarViewBinder nicViewBinder,
             CommandQueue commandQueue,
             CarrierConfigTracker carrierConfigTracker,
             CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
@@ -244,10 +238,6 @@
             DumpManager dumpManager,
             StatusBarWindowStateController statusBarWindowStateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            NotificationIconContainerStatusBarViewModel statusBarIconsViewModel,
-            ConfigurationState configurationState,
-            SystemBarUtilsState systemBarUtilsState,
-            StatusBarNotificationIconViewStore statusBarIconViewStore,
             DemoModeController demoModeController) {
         mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
         mOngoingCallController = ongoingCallController;
@@ -263,7 +253,7 @@
         mKeyguardStateController = keyguardStateController;
         mShadeViewController = shadeViewController;
         mStatusBarStateController = statusBarStateController;
-        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
+        mNicViewBinder = nicViewBinder;
         mCommandQueue = commandQueue;
         mCarrierConfigTracker = carrierConfigTracker;
         mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger;
@@ -273,10 +263,6 @@
         mDumpManager = dumpManager;
         mStatusBarWindowStateController = statusBarWindowStateController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mStatusBarIconsViewModel = statusBarIconsViewModel;
-        mConfigurationState = configurationState;
-        mSystemBarUtilsState = systemBarUtilsState;
-        mStatusBarIconViewStore = statusBarIconViewStore;
         mDemoModeController = demoModeController;
     }
 
@@ -455,24 +441,24 @@
             mStartableStates.put(startable, Startable.State.STOPPED);
         }
         mDumpManager.unregisterDumpable(getClass().getSimpleName());
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            if (mNicBindingDisposable != null) {
+                mNicBindingDisposable.dispose();
+                mNicBindingDisposable = null;
+            }
+        }
     }
 
     /** Initializes views related to the notification icon area. */
     public void initNotificationIconArea() {
         ViewGroup notificationIconArea = mStatusBar.requireViewById(R.id.notification_icon_area);
         if (NotificationIconContainerRefactor.isEnabled()) {
-            mNotificationIconAreaInner =
-                LayoutInflater.from(getContext())
-                        .inflate(R.layout.notification_icon_area, notificationIconArea, true);
+            LayoutInflater.from(getContext())
+                    .inflate(R.layout.notification_icon_area, notificationIconArea, true);
             NotificationIconContainer notificationIcons =
                     notificationIconArea.requireViewById(R.id.notificationIcons);
-            NotificationIconContainerViewBinder.bindWhileAttached(
-                    notificationIcons,
-                    mStatusBarIconsViewModel,
-                    mConfigurationState,
-                    mSystemBarUtilsState,
-                    mIconViewBindingFailureTracker,
-                    mStatusBarIconViewStore);
+            mNotificationIconAreaInner = notificationIcons;
+            mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons);
         } else {
             mNotificationIconAreaInner =
                     mNotificationIconAreaController.getNotificationInnerAreaView();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index 425da5f..48bf7ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -27,6 +27,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -48,7 +49,7 @@
     override val subId: Int,
     startingIsCarrierMerged: Boolean,
     override val tableLogBuffer: TableLogBuffer,
-    subscriptionModel: StateFlow<SubscriptionModel?>,
+    subscriptionModel: Flow<SubscriptionModel?>,
     private val defaultNetworkName: NetworkNameModel,
     private val networkNameSeparator: String,
     @Application scope: CoroutineScope,
@@ -331,7 +332,7 @@
         fun build(
             subId: Int,
             startingIsCarrierMerged: Boolean,
-            subscriptionModel: StateFlow<SubscriptionModel?>,
+            subscriptionModel: Flow<SubscriptionModel?>,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
         ): FullMobileConnectionRepository {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 4fb99c24..be2c21b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -96,7 +96,7 @@
 class MobileConnectionRepositoryImpl(
     override val subId: Int,
     private val context: Context,
-    subscriptionModel: StateFlow<SubscriptionModel?>,
+    subscriptionModel: Flow<SubscriptionModel?>,
     defaultNetworkName: NetworkNameModel,
     networkNameSeparator: String,
     connectivityManager: ConnectivityManager,
@@ -448,7 +448,7 @@
         fun build(
             subId: Int,
             mobileLogger: TableLogBuffer,
-            subscriptionModel: StateFlow<SubscriptionModel?>,
+            subscriptionModel: Flow<SubscriptionModel?>,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
         ): MobileConnectionRepository {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 2a510e4..a455db2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -357,10 +357,10 @@
 
     @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
 
-    private fun subscriptionModelForSubId(subId: Int): StateFlow<SubscriptionModel?> {
-        return subscriptions
-            .map { list -> list.firstOrNull { model -> model.subscriptionId == subId } }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+    private fun subscriptionModelForSubId(subId: Int): Flow<SubscriptionModel?> {
+        return subscriptions.map { list ->
+            list.firstOrNull { model -> model.subscriptionId == subId }
+        }
     }
 
     private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index a14e87c..5475528 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -30,6 +30,7 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.Flags.statusBarStaticInoutIndicators
 import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -41,6 +42,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_ACTIVE
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_INACTIVE
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -190,10 +193,29 @@
                         }
                     }
 
-                    // Set the activity indicators
-                    launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } }
+                    if (statusBarStaticInoutIndicators()) {
+                        // Set the opacity of the activity indicators
+                        launch {
+                            viewModel.activityInVisible.collect { visible ->
+                                activityIn.imageAlpha =
+                                    (if (visible) ALPHA_ACTIVE else ALPHA_INACTIVE)
+                            }
+                        }
 
-                    launch { viewModel.activityOutVisible.collect { activityOut.isVisible = it } }
+                        launch {
+                            viewModel.activityOutVisible.collect { visible ->
+                                activityOut.imageAlpha =
+                                    (if (visible) ALPHA_ACTIVE else ALPHA_INACTIVE)
+                            }
+                        }
+                    } else {
+                        // Set the activity indicators
+                        launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } }
+
+                        launch {
+                            viewModel.activityOutVisible.collect { activityOut.isVisible = it }
+                        }
+                    }
 
                     launch {
                         viewModel.activityContainerVisible.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index d88c9ef..60c662d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
 import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
+import com.android.systemui.Flags.statusBarStaticInoutIndicators
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.flags.FeatureFlagsClassic
@@ -199,7 +200,10 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val activityContainerVisible: Flow<Boolean> =
-        activity
-            .map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+        if (statusBarStaticInoutIndicators()) {
+                flowOf(constants.shouldShowActivityConfig)
+            } else {
+                activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarViewBinderConstants.kt
similarity index 66%
rename from packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarViewBinderConstants.kt
index dca151d..7348d5f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarViewBinderConstants.kt
@@ -13,14 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.systemui.statusbar.pipeline.shared.ui.binder
 
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.graphics.Rect
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@ExperimentalCoroutinesApi
-class UdfpsKeyguardViewModel @Inject constructor() {
-    var sensorBounds: Rect = Rect()
+object StatusBarViewBinderConstants {
+    /** Opacity for the traffic indicator icons */
+    const val ALPHA_ACTIVE: Int = (1.0 * 255).toInt() // Fully opaque
+    const val ALPHA_INACTIVE: Int = (0.3 * 255).toInt() // 30% opaque -> 70% transparency
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 6005527..9512490 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -23,6 +23,7 @@
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.statusBarStaticInoutIndicators
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
@@ -30,6 +31,8 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_ACTIVE
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_INACTIVE
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import kotlinx.coroutines.InternalCoroutinesApi
@@ -118,15 +121,36 @@
 
                 launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } }
 
-                launch {
-                    viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible ->
-                        activityInView.isVisible = visible
+                if (statusBarStaticInoutIndicators()) {
+                    // Set the opacity of the activity indicators
+                    launch {
+                        viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible
+                            ->
+                            activityInView.imageAlpha =
+                                (if (visible) ALPHA_ACTIVE else ALPHA_INACTIVE)
+                        }
                     }
-                }
 
-                launch {
-                    viewModel.isActivityOutViewVisible.distinctUntilChanged().collect { visible ->
-                        activityOutView.isVisible = visible
+                    launch {
+                        viewModel.isActivityOutViewVisible.distinctUntilChanged().collect { visible
+                            ->
+                            activityOutView.imageAlpha =
+                                (if (visible) ALPHA_ACTIVE else ALPHA_INACTIVE)
+                        }
+                    }
+                } else {
+                    launch {
+                        viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible
+                            ->
+                            activityInView.isVisible = visible
+                        }
+                    }
+
+                    launch {
+                        viewModel.isActivityOutViewVisible.distinctUntilChanged().collect { visible
+                            ->
+                            activityOutView.isVisible = visible
+                        }
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index d099c8e..fa243da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.content.Context
+import com.android.systemui.Flags.statusBarStaticInoutIndicators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.table.TableLogBuffer
@@ -38,7 +39,6 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
@@ -101,42 +101,36 @@
             )
 
     /** The wifi activity status. Null if we shouldn't display the activity status. */
-    private val activity: Flow<DataActivityModel> = run {
-        val default = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+    private val activity: Flow<DataActivityModel?> =
         if (!connectivityConstants.shouldShowActivityConfig) {
-                flowOf(default)
+                flowOf(null)
             } else {
                 combine(interactor.activity, interactor.ssid) { activity, ssid ->
                     when (ssid) {
-                        null -> default
+                        null -> null
                         else -> activity
                     }
                 }
             }
-            .distinctUntilChanged()
-            .logDiffsForTable(
-                wifiTableLogBuffer,
-                columnPrefix = "VM.activity",
-                initialValue = default,
-            )
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = default)
-    }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
 
     override val isActivityInViewVisible: Flow<Boolean> =
         activity
-            .map { it.hasActivityIn }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+            .map { it?.hasActivityIn ?: false }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val isActivityOutViewVisible: Flow<Boolean> =
         activity
-            .map { it.hasActivityOut }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+            .map { it?.hasActivityOut ?: false }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val isActivityContainerVisible: Flow<Boolean> =
-        combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut ->
-                activityIn || activityOut
+        if (statusBarStaticInoutIndicators()) {
+                flowOf(connectivityConstants.shouldShowActivityConfig)
+            } else {
+                activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
             }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the
     //  airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 4864fb8..5bced93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -303,8 +303,7 @@
                     mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null
                             && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime());
                     if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
-                        // Pass null to ensure all inputs are cleared for this entry b/227115380
-                            mController.removeRemoteInput(mEntry, null,
+                            mController.removeRemoteInput(mEntry, mToken,
                                     /* reason= */"RemoteInputView$WindowInsetAnimation#onEnd");
                     }
                 }
@@ -536,6 +535,11 @@
         if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) {
             return;
         }
+        // RemoteInputView can be detached from window before IME close event in some cases like
+        // remote input view removal with notification update. As a result of this, RemoteInputView
+        // will stop ime animation updates, which results in never removing remote input. That's why
+        // we have to set mRemoteEditImeAnimatingAway false on detach to remove remote input.
+        mEntry.mRemoteEditImeAnimatingAway = false;
         mController.removeRemoteInput(mEntry, mToken,
                 /* reason= */"RemoteInputView#onDetachedFromWindow");
         mController.removeSpinning(mEntry.getKey(), mToken);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index fa0cb5c..66bf527 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy;
 
 import android.app.AlarmManager;
+import android.app.Flags;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -191,7 +192,11 @@
 
     @Override
     public void setZen(int zen, Uri conditionId, String reason) {
-        mNoMan.setZenMode(zen, conditionId, reason);
+        if (Flags.modesApi()) {
+            mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true);
+        } else {
+            mNoMan.setZenMode(zen, conditionId, reason);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastModule.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastModule.kt
new file mode 100644
index 0000000..6cd9993
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.toast
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+interface ToastModule {
+    /** Starts ToastUI. */
+    @Binds
+    @IntoMap
+    @ClassKey(ToastUI::class)
+    fun bindToastUIStartable(service: ToastUI): CoreStartable
+
+    /** Listen to config changes for ToastUI. */
+    @Binds @IntoSet fun bindToastUIConfigChanges(service: ToastUI): ConfigurationListener
+}
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index 27f8121..85a455d 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -42,6 +42,7 @@
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import java.util.Objects;
 
@@ -51,7 +52,10 @@
  * Controls display of text toasts.
  */
 @SysUISingleton
-public class ToastUI implements CoreStartable, CommandQueue.Callbacks {
+public class ToastUI implements
+        CoreStartable,
+        ConfigurationController.ConfigurationListener,
+        CommandQueue.Callbacks {
     // values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY
     private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds
     private static final int TOAST_SHORT_TIME = 2000; // 2 seconds
@@ -187,7 +191,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         if (newConfig.orientation != mOrientation) {
             mOrientation = newConfig.orientation;
             if (mToast != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index e0d205f..c170eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.internal.util.UserIcons
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.switchUserOnBg
 import com.android.systemui.SystemUISecondaryUserService
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -44,6 +45,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.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -100,6 +102,7 @@
     broadcastDispatcher: BroadcastDispatcher,
     keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val activityManager: ActivityManager,
     private val refreshUsersScheduler: RefreshUsersScheduler,
     private val guestUserInteractor: GuestUserInteractor,
@@ -339,7 +342,11 @@
             }
             .launchIn(applicationScope)
         restartSecondaryService(repository.getSelectedUserInfo().id)
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        applicationScope.launch {
+            withContext(mainDispatcher) {
+                keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+            }
+        }
     }
 
     fun addCallback(callback: UserCallback) {
@@ -593,10 +600,18 @@
     private fun switchUser(userId: Int) {
         // TODO(b/246631653): track jank and latency like in the old impl.
         refreshUsersScheduler.pause()
-        try {
-            activityManager.switchUser(userId)
-        } catch (e: RemoteException) {
-            Log.e(TAG, "Couldn't switch user.", e)
+        val runnable = Runnable {
+            try {
+                activityManager.switchUser(userId)
+            } catch (e: RemoteException) {
+                Log.e(TAG, "Couldn't switch user.", e)
+            }
+        }
+
+        if (switchUserOnBg()) {
+            applicationScope.launch { withContext(backgroundDispatcher) { runnable.run() } }
+        } else {
+            runnable.run()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
index 472f0ae..cf6b0d9 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
@@ -5,9 +5,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dagger.qualifiers.Tracing
-import com.android.systemui.Flags.coroutineTracing
-import com.android.app.tracing.TraceUtils.Companion.coroutineTracingIsEnabled
-import com.android.app.tracing.TraceContextElement
+import com.android.app.tracing.coroutines.createCoroutineTracingContext
 import dagger.Module
 import dagger.Provides
 import kotlinx.coroutines.CoroutineDispatcher
@@ -16,7 +14,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.plus
 import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
 
 /** Providers for various coroutines-related constructs. */
 @Module
@@ -83,9 +80,6 @@
     @Tracing
     @SysUISingleton
     fun tracingCoroutineContext(): CoroutineContext {
-        return if (coroutineTracing()) {
-            coroutineTracingIsEnabled = true
-            TraceContextElement()
-        } else EmptyCoroutineContext
+        return createCoroutineTracingContext()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
deleted file mode 100644
index 8215360..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.util.leak;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.content.FileProvider;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-/**
- * Utility class for dumping, compressing, sending, and serving heap dump files.
- *
- * <p>Unlike the Internet, this IS a big truck you can dump something on.
- */
-public class DumpTruck {
-    private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
-    private static final String FILEPROVIDER_PATH = "leak";
-
-    private static final String TAG = "DumpTruck";
-    private static final int BUFSIZ = 1024 * 1024; // 1MB
-
-    private final Context context;
-    private final GarbageMonitor mGarbageMonitor;
-    private Uri hprofUri;
-    private long rss;
-    final StringBuilder body = new StringBuilder();
-
-    public DumpTruck(Context context, GarbageMonitor garbageMonitor) {
-        this.context = context;
-        mGarbageMonitor = garbageMonitor;
-    }
-
-    /**
-     * Capture memory for the given processes and zip them up for sharing.
-     *
-     * @param pids
-     * @return this, for chaining
-     */
-    public DumpTruck captureHeaps(List<Long> pids) {
-        final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH);
-        dumpDir.mkdirs();
-        hprofUri = null;
-
-        body.setLength(0);
-        body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n");
-
-        final ArrayList<String> paths = new ArrayList<String>();
-        final int myPid = android.os.Process.myPid();
-
-        for (Long pidL : pids) {
-            final int pid = pidL.intValue();
-            body.append("  pid ").append(pid);
-            GarbageMonitor.ProcessMemInfo info = mGarbageMonitor.getMemInfo(pid);
-            if (info != null) {
-                body.append(":")
-                        .append(" up=")
-                        .append(info.getUptime())
-                        .append(" rss=")
-                        .append(info.currentRss);
-                rss = info.currentRss;
-            }
-            if (pid == myPid) {
-                final String path =
-                        new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath();
-                Log.v(TAG, "Dumping memory info for process " + pid + " to " + path);
-                try {
-                    android.os.Debug.dumpHprofData(path); // will block
-                    paths.add(path);
-                    body.append(" (hprof attached)");
-                } catch (IOException e) {
-                    Log.e(TAG, "error dumping memory:", e);
-                    body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n");
-                }
-            }
-            body.append("\n");
-        }
-
-        try {
-            final String zipfile =
-                    new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis()))
-                            .getCanonicalPath();
-            if (DumpTruck.zipUp(zipfile, paths)) {
-                final File pathFile = new File(zipfile);
-                hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile);
-                Log.v(TAG, "Heap dump accessible at URI: " + hprofUri);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "unable to zip up heapdumps", e);
-            body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n");
-        }
-
-        return this;
-    }
-
-    /**
-     * Get the Uri of the current heap dump. Be sure to call captureHeaps first.
-     *
-     * @return Uri to the dump served by the SystemUI file provider
-     */
-    public Uri getDumpUri() {
-        return hprofUri;
-    }
-
-    /**
-     * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification.
-     *
-     * @return share intent
-     */
-    public Intent createShareIntent() {
-        Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE);
-        shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        shareIntent.putExtra(Intent.EXTRA_SUBJECT,
-                String.format("SystemUI memory dump (rss=%dM)", rss / 1024));
-
-        shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString());
-
-        if (hprofUri != null) {
-            final ArrayList<Uri> uriList = new ArrayList<>();
-            uriList.add(hprofUri);
-            shareIntent.setType("application/zip");
-            shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
-
-            // Include URI in ClipData also, so that grantPermission picks it up.
-            // We don't use setData here because some apps interpret this as "to:".
-            ClipData clipdata = new ClipData(new ClipDescription("content",
-                    new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
-                    new ClipData.Item(hprofUri));
-            shareIntent.setClipData(clipdata);
-            shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        }
-        return shareIntent;
-    }
-
-    private static boolean zipUp(String zipfilePath, ArrayList<String> paths) {
-        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) {
-            final byte[] buf = new byte[BUFSIZ];
-
-            for (String filename : paths) {
-                try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) {
-                    ZipEntry entry = new ZipEntry(filename);
-                    zos.putNextEntry(entry);
-                    int len;
-                    while (0 < (len = is.read(buf, 0, BUFSIZ))) {
-                        zos.write(buf, 0, len);
-                    }
-                    zos.closeEntry();
-                }
-            }
-            return true;
-        } catch (IOException e) {
-            Log.e(TAG, "error zipping up profile data", e);
-        }
-        return false;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
deleted file mode 100644
index de392d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ /dev/null
@@ -1,617 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.util.leak;
-
-import static android.service.quicksettings.Tile.STATE_ACTIVE;
-import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
-
-import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.os.SystemProperties;
-import android.provider.Settings;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.util.LongSparseArray;
-import android.view.View;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.QsEventLogger;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.concurrency.MessageRouter;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * Suite of tools to periodically inspect the System UI heap and possibly prompt the user to
- * capture heap dumps and report them. Includes the implementation of the "Dump SysUI Heap"
- * quick settings tile.
- */
-@SysUISingleton
-public class GarbageMonitor implements Dumpable {
-    // Feature switches
-    // ================
-
-    // Whether to use TrackedGarbage to trigger LeakReporter. Off by default unless you set the
-    // appropriate sysprop on a userdebug device.
-    public static final boolean LEAK_REPORTING_ENABLED = Build.IS_DEBUGGABLE
-            && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
-    public static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
-
-    // Heap tracking: watch the current memory levels and update the MemoryTile if available.
-    // On for all userdebug devices.
-    public static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
-
-    // Tell QSTileHost.java to toss this into the default tileset?
-    public static final boolean ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS = true;
-
-    // whether to use ActivityManager.setHeapLimit (and post a notification to the user asking
-    // to dump the heap). Off by default unless you set the appropriate sysprop on userdebug
-    private static final boolean ENABLE_AM_HEAP_LIMIT = Build.IS_DEBUGGABLE
-            && SystemProperties.getBoolean("debug.enable_sysui_heap_limit", false);
-
-    // Tuning params
-    // =============
-
-    // threshold for setHeapLimit(), in KB (overrides R.integer.watch_heap_limit)
-    private static final String SETTINGS_KEY_AM_HEAP_LIMIT = "systemui_am_heap_limit";
-
-    private static final long GARBAGE_INSPECTION_INTERVAL =
-            15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
-    private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
-    private static final int HEAP_TRACK_HISTORY_LEN = 720; // 12 hours
-
-    private static final int DO_GARBAGE_INSPECTION = 1000;
-    private static final int DO_HEAP_TRACK = 3000;
-
-    static final int GARBAGE_ALLOWANCE = 5;
-
-    private static final String TAG = "GarbageMonitor";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
-    private final MessageRouter mMessageRouter;
-    private final TrackedGarbage mTrackedGarbage;
-    private final LeakReporter mLeakReporter;
-    private final Context mContext;
-    private final DelayableExecutor mDelayableExecutor;
-    private MemoryTile mQSTile;
-    private final DumpTruck mDumpTruck;
-
-    private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
-    private final ArrayList<Long> mPids = new ArrayList<>();
-
-    private long mHeapLimit;
-
-    /**
-     */
-    @Inject
-    public GarbageMonitor(
-            Context context,
-            @Background DelayableExecutor delayableExecutor,
-            @Background MessageRouter messageRouter,
-            LeakDetector leakDetector,
-            LeakReporter leakReporter,
-            DumpManager dumpManager) {
-        mContext = context.getApplicationContext();
-
-        mDelayableExecutor = delayableExecutor;
-        mMessageRouter = messageRouter;
-        mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection);
-        mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack);
-
-        mTrackedGarbage = leakDetector.getTrackedGarbage();
-        mLeakReporter = leakReporter;
-
-        mDumpTruck = new DumpTruck(mContext, this);
-
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
-
-        if (ENABLE_AM_HEAP_LIMIT) {
-            mHeapLimit = Settings.Global.getInt(context.getContentResolver(),
-                    SETTINGS_KEY_AM_HEAP_LIMIT,
-                    mContext.getResources().getInteger(R.integer.watch_heap_limit));
-        }
-    }
-
-    public void startLeakMonitor() {
-        if (mTrackedGarbage == null) {
-            return;
-        }
-
-        mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION);
-    }
-
-    public void startHeapTracking() {
-        startTrackingProcess(
-                android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
-        mMessageRouter.sendMessage(DO_HEAP_TRACK);
-    }
-
-    private boolean gcAndCheckGarbage() {
-        if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
-            Runtime.getRuntime().gc();
-            return true;
-        }
-        return false;
-    }
-
-    void reinspectGarbageAfterGc() {
-        int count = mTrackedGarbage.countOldGarbage();
-        if (count > GARBAGE_ALLOWANCE) {
-            mLeakReporter.dumpLeak(count);
-        }
-    }
-
-    public ProcessMemInfo getMemInfo(int pid) {
-        return mData.get(pid);
-    }
-
-    public List<Long> getTrackedProcesses() {
-        return mPids;
-    }
-
-    public void startTrackingProcess(long pid, String name, long start) {
-        synchronized (mPids) {
-            if (mPids.contains(pid)) return;
-
-            mPids.add(pid);
-            logPids();
-
-            mData.put(pid, new ProcessMemInfo(pid, name, start));
-        }
-    }
-
-    private void logPids() {
-        if (DEBUG) {
-            StringBuffer sb = new StringBuffer("Now tracking processes: ");
-            for (int i = 0; i < mPids.size(); i++) {
-                final int p = mPids.get(i).intValue();
-                sb.append(" ");
-            }
-            Log.v(TAG, sb.toString());
-        }
-    }
-
-    private void update() {
-        synchronized (mPids) {
-            for (int i = 0; i < mPids.size(); i++) {
-                final int pid = mPids.get(i).intValue();
-                // rssValues contains [VmRSS, RssFile, RssAnon, VmSwap].
-                long[] rssValues = Process.getRss(pid);
-                if (rssValues == null && rssValues.length == 0) {
-                    if (DEBUG) Log.e(TAG, "update: Process.getRss() didn't provide any values.");
-                    break;
-                }
-                long rss = rssValues[0];
-                final ProcessMemInfo info = mData.get(pid);
-                info.rss[info.head] = info.currentRss = rss;
-                info.head = (info.head + 1) % info.rss.length;
-                if (info.currentRss > info.max) info.max = info.currentRss;
-                if (info.currentRss == 0) {
-                    if (DEBUG) Log.v(TAG, "update: pid " + pid + " has rss=0, it probably died");
-                    mData.remove(pid);
-                }
-            }
-            for (int i = mPids.size() - 1; i >= 0; i--) {
-                final long pid = mPids.get(i).intValue();
-                if (mData.get(pid) == null) {
-                    mPids.remove(i);
-                    logPids();
-                }
-            }
-        }
-        if (mQSTile != null) mQSTile.update();
-    }
-
-    private void setTile(MemoryTile tile) {
-        mQSTile = tile;
-        if (tile != null) tile.update();
-    }
-
-    private static String formatBytes(long b) {
-        String[] SUFFIXES = {"B", "K", "M", "G", "T"};
-        int i;
-        for (i = 0; i < SUFFIXES.length; i++) {
-            if (b < 1024) break;
-            b /= 1024;
-        }
-        return b + SUFFIXES[i];
-    }
-
-    private Intent dumpHprofAndGetShareIntent() {
-        return mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
-    }
-
-    @Override
-    public void dump(PrintWriter pw, @Nullable String[] args) {
-        pw.println("GarbageMonitor params:");
-        pw.println(String.format("   mHeapLimit=%d KB", mHeapLimit));
-        pw.println(String.format("   GARBAGE_INSPECTION_INTERVAL=%d (%.1f mins)",
-                GARBAGE_INSPECTION_INTERVAL,
-                (float) GARBAGE_INSPECTION_INTERVAL / DateUtils.MINUTE_IN_MILLIS));
-        final float htiMins = HEAP_TRACK_INTERVAL / DateUtils.MINUTE_IN_MILLIS;
-        pw.println(String.format("   HEAP_TRACK_INTERVAL=%d (%.1f mins)",
-                HEAP_TRACK_INTERVAL,
-                htiMins));
-        pw.println(String.format("   HEAP_TRACK_HISTORY_LEN=%d (%.1f hr total)",
-                HEAP_TRACK_HISTORY_LEN,
-                (float) HEAP_TRACK_HISTORY_LEN * htiMins / 60f));
-
-        pw.println("GarbageMonitor tracked processes:");
-
-        for (long pid : mPids) {
-            final ProcessMemInfo pmi = mData.get(pid);
-            if (pmi != null) {
-                pmi.dump(pw, args);
-            }
-        }
-    }
-
-
-    private static class MemoryIconDrawable extends Drawable {
-        long rss, limit;
-        final Drawable baseIcon;
-        final Paint paint = new Paint();
-        final float dp;
-
-        MemoryIconDrawable(Context context) {
-            baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
-            dp = context.getResources().getDisplayMetrics().density;
-            paint.setColor(Color.WHITE);
-        }
-
-        public void setRss(long rss) {
-            if (rss != this.rss) {
-                this.rss = rss;
-                invalidateSelf();
-            }
-        }
-
-        public void setLimit(long limit) {
-            if (limit != this.limit) {
-                this.limit = limit;
-                invalidateSelf();
-            }
-        }
-
-        @Override
-        public void draw(Canvas canvas) {
-            baseIcon.draw(canvas);
-
-            if (limit > 0 && rss > 0) {
-                float frac = Math.min(1f, (float) rss / limit);
-
-                final Rect bounds = getBounds();
-                canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
-                //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
-                canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
-            }
-        }
-
-        @Override
-        public void setBounds(int left, int top, int right, int bottom) {
-            super.setBounds(left, top, right, bottom);
-            baseIcon.setBounds(left, top, right, bottom);
-        }
-
-        @Override
-        public int getIntrinsicHeight() {
-            return baseIcon.getIntrinsicHeight();
-        }
-
-        @Override
-        public int getIntrinsicWidth() {
-            return baseIcon.getIntrinsicWidth();
-        }
-
-        @Override
-        public void setAlpha(int i) {
-            baseIcon.setAlpha(i);
-        }
-
-        @Override
-        public void setColorFilter(ColorFilter colorFilter) {
-            baseIcon.setColorFilter(colorFilter);
-            paint.setColorFilter(colorFilter);
-        }
-
-        @Override
-        public void setTint(int tint) {
-            super.setTint(tint);
-            baseIcon.setTint(tint);
-        }
-
-        @Override
-        public void setTintList(ColorStateList tint) {
-            super.setTintList(tint);
-            baseIcon.setTintList(tint);
-        }
-
-        @Override
-        public void setTintMode(PorterDuff.Mode tintMode) {
-            super.setTintMode(tintMode);
-            baseIcon.setTintMode(tintMode);
-        }
-
-        @Override
-        public int getOpacity() {
-            return PixelFormat.TRANSLUCENT;
-        }
-    }
-
-    private static class MemoryGraphIcon extends QSTile.Icon {
-        long rss, limit;
-
-        public void setRss(long rss) {
-            this.rss = rss;
-        }
-
-        public void setHeapLimit(long limit) {
-            this.limit = limit;
-        }
-
-        @Override
-        public Drawable getDrawable(Context context) {
-            final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
-            drawable.setRss(rss);
-            drawable.setLimit(limit);
-            return drawable;
-        }
-    }
-
-    public static class MemoryTile extends QSTileImpl<QSTile.State> {
-        public static final String TILE_SPEC = "dbg:mem";
-
-        private final GarbageMonitor gm;
-        private ProcessMemInfo pmi;
-        private boolean dumpInProgress;
-        private final PanelInteractor mPanelInteractor;
-
-        @Inject
-        public MemoryTile(
-                QSHost host,
-                QsEventLogger uiEventLogger,
-                @Background Looper backgroundLooper,
-                @Main Handler mainHandler,
-                FalsingManager falsingManager,
-                MetricsLogger metricsLogger,
-                StatusBarStateController statusBarStateController,
-                ActivityStarter activityStarter,
-                QSLogger qsLogger,
-                GarbageMonitor monitor,
-                PanelInteractor panelInteractor
-        ) {
-            super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
-                    statusBarStateController, activityStarter, qsLogger);
-            gm = monitor;
-            mPanelInteractor = panelInteractor;
-        }
-
-        @Override
-        public State newTileState() {
-            return new QSTile.State();
-        }
-
-        @Override
-        public Intent getLongClickIntent() {
-            return new Intent();
-        }
-
-        @Override
-        protected void handleClick(@Nullable View view) {
-            if (dumpInProgress) return;
-
-            dumpInProgress = true;
-            refreshState();
-            new Thread("HeapDumpThread") {
-                @Override
-                public void run() {
-                    try {
-                        // wait for animations & state changes
-                        Thread.sleep(500);
-                    } catch (InterruptedException ignored) { }
-                    final Intent shareIntent = gm.dumpHprofAndGetShareIntent();
-                    mHandler.post(() -> {
-                        dumpInProgress = false;
-                        refreshState();
-                        mPanelInteractor.collapsePanels();
-                        mActivityStarter.postStartActivityDismissingKeyguard(shareIntent, 0);
-                    });
-                }
-            }.start();
-        }
-
-        @Override
-        public int getMetricsCategory() {
-            return VIEW_UNKNOWN;
-        }
-
-        @Override
-        public void handleSetListening(boolean listening) {
-            super.handleSetListening(listening);
-            if (gm != null) gm.setTile(listening ? this : null);
-
-            final ActivityManager am = mContext.getSystemService(ActivityManager.class);
-            if (listening && gm.mHeapLimit > 0) {
-                am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
-            } else {
-                am.clearWatchHeapLimit();
-            }
-        }
-
-        @Override
-        public CharSequence getTileLabel() {
-            return getState().label;
-        }
-
-        @Override
-        protected void handleUpdateState(State state, Object arg) {
-            pmi = gm.getMemInfo(Process.myPid());
-            final MemoryGraphIcon icon = new MemoryGraphIcon();
-            icon.setHeapLimit(gm.mHeapLimit);
-            state.state = dumpInProgress ? STATE_UNAVAILABLE : STATE_ACTIVE;
-            state.label = dumpInProgress
-                    ? "Dumping..."
-                    : mContext.getString(R.string.heap_dump_tile_name);
-            if (pmi != null) {
-                icon.setRss(pmi.currentRss);
-                state.secondaryLabel =
-                        String.format(
-                                "rss: %s / %s",
-                                formatBytes(pmi.currentRss * 1024),
-                                formatBytes(gm.mHeapLimit * 1024));
-            } else {
-                icon.setRss(0);
-                state.secondaryLabel = null;
-            }
-            state.icon = icon;
-        }
-
-        public void update() {
-            refreshState();
-        }
-
-        public long getRss() {
-            return pmi != null ? pmi.currentRss : 0;
-        }
-
-        public long getHeapLimit() {
-            return gm != null ? gm.mHeapLimit : 0;
-        }
-    }
-
-    /** */
-    public static class ProcessMemInfo implements Dumpable {
-        public long pid;
-        public String name;
-        public long startTime;
-        public long currentRss;
-        public long[] rss = new long[HEAP_TRACK_HISTORY_LEN];
-        public long max = 1;
-        public int head = 0;
-
-        public ProcessMemInfo(long pid, String name, long start) {
-            this.pid = pid;
-            this.name = name;
-            this.startTime = start;
-        }
-
-        public long getUptime() {
-            return System.currentTimeMillis() - startTime;
-        }
-
-        @Override
-        public void dump(PrintWriter pw, @Nullable String[] args) {
-            pw.print("{ \"pid\": ");
-            pw.print(pid);
-            pw.print(", \"name\": \"");
-            pw.print(name.replace('"', '-'));
-            pw.print("\", \"start\": ");
-            pw.print(startTime);
-            pw.print(", \"rss\": [");
-            // write rss values starting from the oldest, which is rss[head], wrapping around to
-            // rss[(head-1) % rss.length]
-            for (int i = 0; i < rss.length; i++) {
-                if (i > 0) pw.print(",");
-                pw.print(rss[(head + i) % rss.length]);
-            }
-            pw.println("] }");
-        }
-    }
-
-    /** */
-    @SysUISingleton
-    public static class Service implements CoreStartable,  Dumpable {
-        private final Context mContext;
-        private final GarbageMonitor mGarbageMonitor;
-
-        @Inject
-        public Service(Context context, GarbageMonitor garbageMonitor) {
-            mContext = context;
-            mGarbageMonitor = garbageMonitor;
-        }
-
-        @Override
-        public void start() {
-            boolean forceEnable =
-                    Settings.Secure.getInt(
-                                    mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
-                            != 0;
-            if (LEAK_REPORTING_ENABLED || forceEnable) {
-                mGarbageMonitor.startLeakMonitor();
-            }
-            if (HEAP_TRACKING_ENABLED || forceEnable) {
-                mGarbageMonitor.startHeapTracking();
-            }
-        }
-
-        @Override
-        public void dump(PrintWriter pw, @Nullable String[] args) {
-            if (mGarbageMonitor != null) mGarbageMonitor.dump(pw, args);
-        }
-    }
-
-    private void doGarbageInspection(int id) {
-        if (gcAndCheckGarbage()) {
-            mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100);
-        }
-
-        mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION);
-        mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
-    }
-
-    private void doHeapTrack(int id) {
-        update();
-        mMessageRouter.cancelMessages(DO_HEAP_TRACK);
-        mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
deleted file mode 100644
index e975200..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.leak
-
-import com.android.systemui.CoreStartable
-import com.android.systemui.qs.tileimpl.QSTileImpl
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import dagger.multibindings.StringKey
-
-@Module
-interface GarbageMonitorModule {
-    /** Inject into GarbageMonitor.Service. */
-    @Binds
-    @IntoMap
-    @ClassKey(GarbageMonitor::class)
-    fun bindGarbageMonitorService(sysui: GarbageMonitor.Service): CoreStartable
-
-    @Binds
-    @IntoMap
-    @StringKey(GarbageMonitor.MemoryTile.TILE_SPEC)
-    fun bindMemoryTile(memoryTile: GarbageMonitor.MemoryTile): QSTileImpl<*>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index 3451ae0..dc2b80c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -22,16 +22,17 @@
 import android.util.Log;
 
 import com.android.systemui.CoreStartable;
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.res.R;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 
 import java.io.PrintWriter;
 
 import javax.inject.Inject;
 
 @SysUISingleton
-public class VolumeUI implements CoreStartable {
+public class VolumeUI implements CoreStartable, ConfigurationController.ConfigurationListener {
     private static final String TAG = "VolumeUI";
     private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -60,7 +61,7 @@
     }
 
     @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigChanged(Configuration newConfig) {
         if (!mEnabled) return;
         mVolumeComponent.onConfigurationChanged(newConfig);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 53217d4..8d06a8f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -21,6 +21,7 @@
 import android.os.Looper;
 
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
@@ -36,15 +37,30 @@
 import com.android.systemui.volume.VolumeDialogComponent;
 import com.android.systemui.volume.VolumeDialogImpl;
 import com.android.systemui.volume.VolumePanelFactory;
+import com.android.systemui.volume.VolumeUI;
 
 import dagger.Binds;
 import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+import dagger.multibindings.IntoSet;
 
 /** Dagger Module for code in the volume package. */
 @Module
 public interface VolumeModule {
+    /** Starts VolumeUI.  */
+    @Binds
+    @IntoMap
+    @ClassKey(VolumeUI.class)
+    CoreStartable bindVolumeUIStartable(VolumeUI impl);
+
+    /** Listen to config changes for VolumeUI. */
+    @Binds
+    @IntoSet
+    ConfigurationController.ConfigurationListener bindVolumeUIConfigChanges(VolumeUI impl);
+
     /** */
     @Binds
     VolumeComponent provideVolumeComponent(VolumeDialogComponent volumeDialogComponent);
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index fdf59664..20fef92 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -317,10 +317,11 @@
         }
 
         private void loadWallpaperAndDrawFrameInternal() {
-            Trace.beginSection("ImageWallpaper.CanvasEngine#loadWallpaper");
+            Trace.beginSection("WPMS.ImageWallpaper.CanvasEngine#loadWallpaper");
             boolean loadSuccess = false;
             Bitmap bitmap;
             try {
+                Trace.beginSection("WPMS.getBitmapAsUser");
                 bitmap = mWallpaperManager.getBitmapAsUser(
                         mUserTracker.getUserId(), false, getSourceFlag(), true);
                 if (bitmap != null
@@ -333,15 +334,22 @@
                 // be loaded, we will go into a cycle. Don't do a build where the
                 // default wallpaper can't be loaded.
                 Log.w(TAG, "Unable to load wallpaper!", exception);
+                Trace.beginSection("WPMS.clearWallpaper");
                 mWallpaperManager.clearWallpaper(getWallpaperFlags(), mUserTracker.getUserId());
+                Trace.endSection();
 
                 try {
+                    Trace.beginSection("WPMS.getBitmapAsUser_defaultWallpaper");
                     bitmap = mWallpaperManager.getBitmapAsUser(
                             mUserTracker.getUserId(), false, getSourceFlag(), true);
                 } catch (RuntimeException | OutOfMemoryError e) {
                     Log.w(TAG, "Unable to load default wallpaper!", e);
                     bitmap = null;
+                } finally {
+                    Trace.endSection();
                 }
+            } finally {
+                Trace.endSection();
             }
 
             if (bitmap == null) {
@@ -355,15 +363,23 @@
                 loadSuccess = true;
                 // recycle the previously loaded bitmap
                 if (mBitmap != null) {
+                    Trace.beginSection("WPMS.mBitmap.recycle");
                     mBitmap.recycle();
+                    Trace.endSection();
                 }
                 mBitmap = bitmap;
+                Trace.beginSection("WPMS.wallpaperSupportsWcg");
                 mWideColorGamut = mWallpaperManager.wallpaperSupportsWcg(getSourceFlag());
+                Trace.endSection();
 
                 // +2 usages for the color extraction and the delayed unload.
                 mBitmapUsages += 2;
+                Trace.beginSection("WPMS.recomputeColorExtractorMiniBitmap");
                 recomputeColorExtractorMiniBitmap();
+                Trace.endSection();
+                Trace.beginSection("WPMS.drawFrameInternal");
                 drawFrameInternal();
+                Trace.endSection();
 
                 /*
                  * after loading, the bitmap will be unloaded after all these conditions:
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 6f58bc2..e6637e6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -30,13 +30,15 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockEvents
 import com.android.systemui.plugins.clocks.ClockFaceConfig
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockTickRate
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -94,9 +96,9 @@
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
     @Mock private lateinit var parentView: View
     private lateinit var repository: FakeKeyguardRepository
-    @Mock private lateinit var smallLogBuffer: LogBuffer
-    @Mock private lateinit var largeLogBuffer: LogBuffer
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    private val messageBuffer = LogcatOnlyMessageBuffer(LogLevel.DEBUG)
+    private val clockBuffers = ClockMessageBuffers(messageBuffer, messageBuffer, messageBuffer)
     private lateinit var underTest: ClockEventController
     @Mock private lateinit var zenModeController: ZenModeController
 
@@ -140,8 +142,7 @@
                 context,
                 mainExecutor,
                 bgExecutor,
-                smallLogBuffer,
-                largeLogBuffer,
+                clockBuffers,
                 withDeps.featureFlags,
                 zenModeController
             )
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index 88f63ad..a249961 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -34,14 +34,12 @@
 import android.widget.RelativeLayout;
 
 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;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.plugins.clocks.ClockAnimations;
 import com.android.systemui.plugins.clocks.ClockController;
@@ -56,14 +54,9 @@
 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.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
-import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 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.ui.SystemBarUtilsState;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -185,9 +178,7 @@
                 mKeyguardSliceViewController,
                 mNotificationIconAreaController,
                 mSmartspaceController,
-                mock(SystemBarUtilsState.class),
-                mock(ScreenOffAnimationController.class),
-                mock(StatusBarIconViewBindingFailureTracker.class),
+                mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class),
                 mKeyguardUnlockAnimationController,
                 mSecureSettings,
                 mExecutor,
@@ -195,11 +186,6 @@
                 mDumpManager,
                 mClockEventController,
                 mLogBuffer,
-                mock(NotificationIconContainerAlwaysOnDisplayViewModel.class),
-                mock(KeyguardRootViewModel.class),
-                mock(ConfigurationState.class),
-                mock(DozeParameters.class),
-                mock(AlwaysOnDisplayNotificationIconViewStore.class),
                 KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(),
                 mKeyguardClockInteractor,
                 mFakeFeatureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt
index b1421b2..e921a59 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/CameraAvailabilityListenerTest.kt
@@ -9,30 +9,34 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.res.R
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Expect
 import java.util.concurrent.Executor
 import kotlin.math.roundToInt
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class CameraAvailabilityListenerTest : SysuiTestCase() {
     companion object {
-        const val EXCLUDED_PKG = "test.excluded.package"
-        const val CAMERA_ID_FRONT = "0"
-        const val CAMERA_ID_INNER = "1"
-        const val PROTECTION_PATH_STRING_FRONT = "M 50,50 a 20,20 0 1 0 40,0 a 20,20 0 1 0 -40,0 Z"
-        const val PROTECTION_PATH_STRING_INNER = "M 40,40 a 10,10 0 1 0 20,0 a 10,10 0 1 0 -20,0 Z"
-        val PATH_RECT_FRONT = rectFromPath(pathFromString(PROTECTION_PATH_STRING_FRONT))
-        val PATH_RECT_INNER = rectFromPath(pathFromString(PROTECTION_PATH_STRING_INNER))
+        private const val EXCLUDED_PKG = "test.excluded.package"
+        private const val LOGICAL_CAMERA_ID_NOT_SPECIFIED = ""
+        private const val LOGICAL_CAMERA_ID_REAR = "0"
+        private const val LOGICAL_CAMERA_ID_FRONT = "1"
+        private const val PHYSICAL_CAMERA_ID_OUTER_FRONT = "5"
+        private const val PHYSICAL_CAMERA_ID_INNER_FRONT = "6"
+        private const val PROTECTION_PATH_STRING_OUTER_FRONT =
+            "M 50,50 a 20,20 0 1 0 40,0 a 20,20 0 1 0 -40,0 Z"
+        private const val PROTECTION_PATH_STRING_INNER_FRONT =
+            "M 40,40 a 10,10 0 1 0 20,0 a 10,10 0 1 0 -20,0 Z"
+        private val PATH_RECT_FRONT =
+            rectFromPath(pathFromString(PROTECTION_PATH_STRING_OUTER_FRONT))
+        private val PATH_RECT_INNER =
+            rectFromPath(pathFromString(PROTECTION_PATH_STRING_INNER_FRONT))
 
         private fun pathFromString(pathString: String): Path {
             val spec = pathString.trim()
@@ -58,105 +62,310 @@
         }
     }
 
-    @Mock private lateinit var cameraManager: CameraManager
-    @Mock
-    private lateinit var cameraTransitionCb: CameraAvailabilityListener.CameraTransitionCallback
-    private lateinit var cameraAvailabilityListener: CameraAvailabilityListener
+    @get:Rule val expect: Expect = Expect.create()
+
+    private val cameraManager = mock<CameraManager>()
+    private val cameraTransitionCallback = TestCameraTransitionCallback()
+
+    private lateinit var cameraAvailabilityCallback: CameraManager.AvailabilityCallback
 
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        context
-            .getOrCreateTestableResources()
-            .addOverride(R.string.config_cameraProtectionExcludedPackages, EXCLUDED_PKG)
-        context
-            .getOrCreateTestableResources()
-            .addOverride(R.string.config_protectedCameraId, CAMERA_ID_FRONT)
-        context
-            .getOrCreateTestableResources()
-            .addOverride(
-                R.string.config_frontBuiltInDisplayCutoutProtection,
-                PROTECTION_PATH_STRING_FRONT
-            )
-        context
-            .getOrCreateTestableResources()
-            .addOverride(R.string.config_protectedInnerCameraId, CAMERA_ID_INNER)
-        context
-            .getOrCreateTestableResources()
-            .addOverride(
-                R.string.config_innerBuiltInDisplayCutoutProtection,
-                PROTECTION_PATH_STRING_INNER
-            )
-
+        overrideResource(R.string.config_cameraProtectionExcludedPackages, EXCLUDED_PKG)
+        setOuterFrontCameraId(LOGICAL_CAMERA_ID_FRONT)
+        setOuterFrontPhysicalCameraId(PHYSICAL_CAMERA_ID_OUTER_FRONT)
+        overrideResource(
+            R.string.config_frontBuiltInDisplayCutoutProtection,
+            PROTECTION_PATH_STRING_OUTER_FRONT
+        )
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_FRONT)
+        setInnerFrontPhysicalCameraId(PHYSICAL_CAMERA_ID_INNER_FRONT)
+        overrideResource(
+            R.string.config_innerBuiltInDisplayCutoutProtection,
+            PROTECTION_PATH_STRING_INNER_FRONT
+        )
         context.addMockSystemService(CameraManager::class.java, cameraManager)
 
-        cameraAvailabilityListener =
-            CameraAvailabilityListener.Factory.build(context, context.mainExecutor)
-    }
-
-    @Test
-    fun testFrontCamera() {
-        var path: Path? = null
-        var rect: Rect? = null
-        val callback =
-            object : CameraAvailabilityListener.CameraTransitionCallback {
-                override fun onApplyCameraProtection(protectionPath: Path, bounds: Rect) {
-                    path = protectionPath
-                    rect = bounds
-                }
-
-                override fun onHideCameraProtection() {}
+        whenever(
+                cameraManager.registerAvailabilityCallback(
+                    any(Executor::class.java),
+                    any(CameraManager.AvailabilityCallback::class.java)
+                )
+            )
+            .thenAnswer {
+                cameraAvailabilityCallback = it.arguments[1] as CameraManager.AvailabilityCallback
+                return@thenAnswer Unit
             }
-
-        cameraAvailabilityListener.addTransitionCallback(callback)
-        cameraAvailabilityListener.startListening()
-
-        val callbackCaptor = withArgCaptor {
-            verify(cameraManager).registerAvailabilityCallback(any(Executor::class.java), capture())
-        }
-
-        callbackCaptor.onCameraOpened(CAMERA_ID_FRONT, "")
-        assertNotNull(path)
-        assertEquals(PATH_RECT_FRONT, rect)
     }
 
     @Test
-    fun testInnerCamera() {
-        var path: Path? = null
-        var rect: Rect? = null
-        val callback =
-            object : CameraAvailabilityListener.CameraTransitionCallback {
-                override fun onApplyCameraProtection(protectionPath: Path, bounds: Rect) {
-                    path = protectionPath
-                    rect = bounds
-                }
+    fun onCameraOpened_matchesOuterFrontInfo_showsOuterProtection() {
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
 
-                override fun onHideCameraProtection() {}
-            }
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
 
-        cameraAvailabilityListener.addTransitionCallback(callback)
-        cameraAvailabilityListener.startListening()
-
-        val callbackCaptor = withArgCaptor {
-            verify(cameraManager).registerAvailabilityCallback(any(Executor::class.java), capture())
-        }
-
-        callbackCaptor.onCameraOpened(CAMERA_ID_INNER, "")
-        assertNotNull(path)
-        assertEquals(PATH_RECT_INNER, rect)
+        assertOuterProtectionShowing()
     }
 
     @Test
-    fun testExcludedPackage() {
-        cameraAvailabilityListener.addTransitionCallback(cameraTransitionCb)
-        cameraAvailabilityListener.startListening()
+    fun onCameraOpened_matchesInnerFrontInfo_showsInnerProtection() {
+        setOuterFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
 
-        val callbackCaptor = withArgCaptor {
-            verify(cameraManager).registerAvailabilityCallback(any(Executor::class.java), capture())
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        assertInnerProtectionShowing()
+    }
+
+    @Test
+    fun onCameraOpened_doesNotMatchAnyProtectionInfo_doesNotShowProtection() {
+        createAndStartSut()
+
+        openCamera(LOGICAL_CAMERA_ID_REAR)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onCameraOpened_matchesInnerAndOuter_innerUnavailable_showsOuterProtection() {
+        val dupeCameraId = "1"
+        setInnerFrontCameraId(dupeCameraId)
+        setOuterFrontCameraId(dupeCameraId)
+        createAndStartSut()
+
+        setPhysicalCameraUnavailable(dupeCameraId, PHYSICAL_CAMERA_ID_INNER_FRONT)
+        openCamera(dupeCameraId)
+
+        assertOuterProtectionShowing()
+    }
+
+    @Test
+    fun onCameraOpened_matchesInnerAndOuter_outerUnavailable_showsInnerFrontProtection() {
+        val dupeCameraId = "1"
+        setInnerFrontCameraId(dupeCameraId)
+        setOuterFrontCameraId(dupeCameraId)
+        createAndStartSut()
+
+        setPhysicalCameraUnavailable(dupeCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+        openCamera(dupeCameraId)
+
+        assertInnerProtectionShowing()
+    }
+
+    @Test
+    fun onCameraOpened_matchesOuterFrontInfo_packageExcluded_doesNotShowProtection() {
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+
+        openCamera(LOGICAL_CAMERA_ID_FRONT, EXCLUDED_PKG)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onCameraOpened_matchesInnerFrontInfo_packageExcluded_doesNotShowProtection() {
+        setOuterFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+
+        openCamera(LOGICAL_CAMERA_ID_FRONT, EXCLUDED_PKG)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onCameraClosed_matchesActiveOuterFrontProtection_hidesProtection() {
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        closeCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onCameraClosed_matchesActiveInnerFrontProtection_hidesProtection() {
+        setOuterFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        closeCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onCameraClosed_doesNotMatchActiveOuterFrontProtection_keepsShowingProtection() {
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        closeCamera(LOGICAL_CAMERA_ID_REAR)
+
+        assertOuterProtectionShowing()
+    }
+
+    @Test
+    fun onCameraClosed_doesNotMatchActiveInnerFrontProtection_keepsShowingProtection() {
+        setOuterFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        closeCamera(LOGICAL_CAMERA_ID_REAR)
+
+        assertInnerProtectionShowing()
+    }
+
+    @Test
+    fun onPhysicalCameraAvailable_cameraOpen_matchesOuterFront_showsOuterFrontProtection() {
+        val logicalCameraId = "1"
+        setOuterFrontCameraId(logicalCameraId)
+        setInnerFrontCameraId(logicalCameraId)
+        createAndStartSut()
+        setPhysicalCameraUnavailable(logicalCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+        openCamera(logicalCameraId)
+
+        setPhysicalCameraAvailable(logicalCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+
+        assertOuterProtectionShowing()
+    }
+
+    @Test
+    fun onPhysicalCameraAvailable_cameraClosed_matchesOuterFront_doesNotShowProtection() {
+        val logicalCameraId = "1"
+        setOuterFrontCameraId(logicalCameraId)
+        setInnerFrontCameraId(logicalCameraId)
+        createAndStartSut()
+        setPhysicalCameraUnavailable(logicalCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+
+        setPhysicalCameraAvailable(logicalCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onPhysicalCameraAvailable_cameraOpen_matchesInnerFront_showsInnerFrontProtection() {
+        val logicalCameraId = "1"
+        setOuterFrontCameraId(logicalCameraId)
+        setInnerFrontCameraId(logicalCameraId)
+        createAndStartSut()
+        setPhysicalCameraUnavailable(logicalCameraId, PHYSICAL_CAMERA_ID_INNER_FRONT)
+        openCamera(logicalCameraId)
+
+        setPhysicalCameraAvailable(logicalCameraId, PHYSICAL_CAMERA_ID_INNER_FRONT)
+
+        assertInnerProtectionShowing()
+    }
+
+    @Test
+    fun onPhysicalCameraAvailable_cameraClosed_matchesInnerFront_doesNotShowProtection() {
+        val logicalCameraId = "1"
+        setOuterFrontCameraId(logicalCameraId)
+        setInnerFrontCameraId(logicalCameraId)
+        createAndStartSut()
+        setPhysicalCameraUnavailable(logicalCameraId, PHYSICAL_CAMERA_ID_INNER_FRONT)
+
+        setPhysicalCameraAvailable(logicalCameraId, PHYSICAL_CAMERA_ID_INNER_FRONT)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onPhysicalCameraUnavailable_matchesActiveProtection_hidesProtection() {
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        setPhysicalCameraUnavailable(LOGICAL_CAMERA_ID_FRONT, PHYSICAL_CAMERA_ID_OUTER_FRONT)
+
+        assertProtectionNotShowing()
+    }
+
+    @Test
+    fun onPhysicalCameraUnavailable_doesNotMatchActiveProtection_keepsShowingProtection() {
+        setInnerFrontCameraId(LOGICAL_CAMERA_ID_NOT_SPECIFIED)
+        createAndStartSut()
+        openCamera(LOGICAL_CAMERA_ID_FRONT)
+
+        setPhysicalCameraUnavailable(LOGICAL_CAMERA_ID_FRONT, PHYSICAL_CAMERA_ID_INNER_FRONT)
+
+        assertOuterProtectionShowing()
+    }
+
+    private fun openCamera(logicalCameraId: String, packageId: String = "") {
+        cameraAvailabilityCallback.onCameraOpened(logicalCameraId, packageId)
+    }
+
+    private fun closeCamera(logicalCameraId: String) {
+        cameraAvailabilityCallback.onCameraClosed(logicalCameraId)
+    }
+
+    private fun setPhysicalCameraAvailable(logicalCameraId: String, physicalCameraId: String) {
+        cameraAvailabilityCallback.onPhysicalCameraAvailable(logicalCameraId, physicalCameraId)
+    }
+
+    private fun setPhysicalCameraUnavailable(logicalCameraId: String, physicalCameraId: String) {
+        cameraAvailabilityCallback.onPhysicalCameraUnavailable(logicalCameraId, physicalCameraId)
+    }
+
+    private fun assertOuterProtectionShowing() {
+        expect.that(cameraTransitionCallback.shouldShowProtection).isTrue()
+        expect.that(cameraTransitionCallback.protectionPath).isNotNull()
+        expect.that(cameraTransitionCallback.protectionBounds).isEqualTo(PATH_RECT_FRONT)
+    }
+
+    private fun assertInnerProtectionShowing() {
+        expect.that(cameraTransitionCallback.shouldShowProtection).isTrue()
+        expect.that(cameraTransitionCallback.protectionPath).isNotNull()
+        expect.that(cameraTransitionCallback.protectionBounds).isEqualTo(PATH_RECT_INNER)
+    }
+
+    private fun assertProtectionNotShowing() {
+        expect.that(cameraTransitionCallback.shouldShowProtection).isFalse()
+        expect.that(cameraTransitionCallback.protectionBounds).isNull()
+        expect.that(cameraTransitionCallback.protectionPath).isNull()
+    }
+
+    private fun setOuterFrontCameraId(id: String) {
+        overrideResource(R.string.config_protectedCameraId, id)
+    }
+
+    private fun setOuterFrontPhysicalCameraId(id: String) {
+        overrideResource(R.string.config_protectedPhysicalCameraId, id)
+    }
+
+    private fun setInnerFrontCameraId(id: String) {
+        overrideResource(R.string.config_protectedInnerCameraId, id)
+    }
+
+    private fun setInnerFrontPhysicalCameraId(id: String) {
+        overrideResource(R.string.config_protectedInnerPhysicalCameraId, id)
+    }
+
+    private fun createAndStartSut(): CameraAvailabilityListener {
+        return CameraAvailabilityListener.build(context, context.mainExecutor).also {
+            it.addTransitionCallback(cameraTransitionCallback)
+            it.startListening()
         }
-        callbackCaptor.onCameraOpened(CAMERA_ID_FRONT, EXCLUDED_PKG)
+    }
 
-        verify(cameraTransitionCb, never())
-            .onApplyCameraProtection(any(Path::class.java), any(Rect::class.java))
+    private class TestCameraTransitionCallback :
+        CameraAvailabilityListener.CameraTransitionCallback {
+        var shouldShowProtection = false
+        var protectionPath: Path? = null
+        var protectionBounds: Rect? = null
+
+        override fun onApplyCameraProtection(protectionPath: Path, bounds: Rect) {
+            shouldShowProtection = true
+            this.protectionPath = protectionPath
+            this.protectionBounds = bounds
+        }
+
+        override fun onHideCameraProtection() {
+            shouldShowProtection = false
+            protectionPath = null
+            protectionBounds = null
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 639276e..b3eab8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -248,8 +248,8 @@
             }
 
             @Override
-            public void onConfigurationChanged(Configuration newConfig) {
-                super.onConfigurationChanged(newConfig);
+            public void onConfigChanged(Configuration newConfig) {
+                super.onConfigChanged(newConfig);
                 mExecutor.runAllReady();
             }
 
@@ -892,7 +892,7 @@
         // Switch to long edge cutout(left).
         mMockCutoutList.set(0, new CutoutDecorProviderImpl(BOUNDS_POSITION_LEFT));
 
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
         verifyOverlaysExistAndAdded(true, false, false, false, View.VISIBLE);
     }
 
@@ -913,7 +913,7 @@
         // Switch to long edge cutout(left).
         mMockCutoutList.set(0, new CutoutDecorProviderImpl(BOUNDS_POSITION_LEFT));
 
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
         verifyOverlaysExistAndAdded(true, false, true, false, View.VISIBLE);
         verify(mDotViewController, times(2)).initialize(any(), any(), any(), any());
         verify(mDotViewController, times(2)).setShowingListener(null);
@@ -949,7 +949,7 @@
         // top cutout
         mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP));
 
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
 
         // Only top windows should be added.
         verifyOverlaysExistAndAdded(false, true, false, false, View.VISIBLE);
@@ -976,7 +976,7 @@
         // top cutout
         mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP));
 
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
 
         // Both top and bottom windows should be added with VISIBLE because of privacy dot and
         // cutout, but rounded corners visibility shall be gone because of no rounding.
@@ -1013,7 +1013,7 @@
         doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
         mDisplayInfo.rotation = Surface.ROTATION_270;
 
-        mScreenDecorations.onConfigurationChanged(null);
+        mScreenDecorations.onConfigChanged(null);
 
         assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize());
         assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize());
@@ -1145,7 +1145,7 @@
         assertThat(mScreenDecorations.mIsRegistered, is(false));
 
         doReturn(true).when(mScreenDecorations).hasOverlays();
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
         assertThat(mScreenDecorations.mIsRegistered, is(true));
     }
 
@@ -1156,7 +1156,7 @@
         mScreenDecorations.start();
         assertThat(mScreenDecorations.mIsRegistered, is(true));
 
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
         assertThat(mScreenDecorations.mIsRegistered, is(true));
     }
 
@@ -1168,7 +1168,7 @@
         assertThat(mScreenDecorations.mIsRegistered, is(true));
 
         doReturn(false).when(mScreenDecorations).hasOverlays();
-        mScreenDecorations.onConfigurationChanged(new Configuration());
+        mScreenDecorations.onConfigChanged(new Configuration());
         assertThat(mScreenDecorations.mIsRegistered, is(false));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
index f8856c9..bd49927 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
@@ -102,8 +102,9 @@
                 getContext().getMainThreadHandler(), mCommandQueue,
                 mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings,
                 mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger);
-        mMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
-                mContext.getSystemService(DisplayManager.class));
+        mMagnification.mWindowMagnificationControllerSupplier =
+                new FakeWindowMagnificationControllerSupplier(
+                        mContext.getSystemService(DisplayManager.class));
         mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
                 mContext.getSystemService(DisplayManager.class));
 
@@ -201,10 +202,10 @@
         verify(mMagnificationSettingsController).setMagnificationScale(eq(testScale));
     }
 
-    private class FakeControllerSupplier extends
+    private class FakeWindowMagnificationControllerSupplier extends
             DisplayIdIndexSupplier<WindowMagnificationController> {
 
-        FakeControllerSupplier(DisplayManager displayManager) {
+        FakeWindowMagnificationControllerSupplier(DisplayManager displayManager) {
             super(displayManager);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
index d0e1678..3b5cbea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
@@ -124,7 +124,7 @@
                 getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController,
                 mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker,
                 getContext().getSystemService(DisplayManager.class), mA11yLogger);
-        mMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
+        mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class), mWindowMagnificationController);
         mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
                 mContext.getSystemService(DisplayManager.class), mMagnificationSettingsController);
@@ -325,9 +325,9 @@
     @Test
     public void overviewProxyIsConnected_controllerIsAvailable_updateSysUiStateFlag() {
         final WindowMagnificationController mController = mock(WindowMagnificationController.class);
-        mMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
+        mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class), mController);
-        mMagnification.mMagnificationControllerSupplier.get(TEST_DISPLAY);
+        mMagnification.mWindowMagnificationControllerSupplier.get(TEST_DISPLAY);
 
         mOverviewProxyListener.onConnectionChanged(true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 91219f0..0ee0939 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -151,6 +151,7 @@
             )
         udfpsOverlayInteractor =
                 UdfpsOverlayInteractor(
+                        context,
                         authController,
                         selectedUserInteractor,
                         testScope.backgroundScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
index e2aa984..647dae6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
@@ -20,10 +20,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import org.junit.Assert.assertFalse
 import org.junit.Before
@@ -42,8 +41,7 @@
 
     @Mock lateinit var udfpsBpView: UdfpsBpView
     @Mock lateinit var statusBarStateController: StatusBarStateController
-    @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
-    @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+    @Mock lateinit var shadeInteractor: ShadeInteractor
     @Mock lateinit var systemUIDialogManager: SystemUIDialogManager
     @Mock lateinit var dumpManager: DumpManager
 
@@ -55,7 +53,7 @@
             UdfpsBpViewController(
                 udfpsBpView,
                 statusBarStateController,
-                primaryBouncerInteractor,
+                shadeInteractor,
                 systemUIDialogManager,
                 dumpManager
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
index 27d93eb..8f0e910 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt
@@ -24,7 +24,6 @@
 import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
 import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
-import android.platform.test.annotations.RequiresFlagsEnabled
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.shared.model.AuthenticationReason
@@ -48,7 +47,6 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 
-@RequiresFlagsEnabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
@@ -62,6 +60,7 @@
 
     @Before
     fun setUp() {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         underTest = BiometricStatusRepositoryImpl(testScope.backgroundScope, biometricManager)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
index 6978923..d7b7d79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt
@@ -19,7 +19,6 @@
 import android.app.ActivityManager
 import android.app.ActivityTaskManager
 import android.content.ComponentName
-import android.platform.test.annotations.RequiresFlagsEnabled
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
@@ -44,7 +43,6 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 
-@RequiresFlagsEnabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
@@ -59,6 +57,7 @@
 
     @Before
     fun setup() {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         biometricStatusRepository = FakeBiometricStatusRepository()
         underTest = BiometricStatusInteractorImpl(activityTaskManager, biometricStatusRepository)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
index bbf471c..6a68672 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
@@ -108,6 +108,7 @@
     private fun createUdpfsOverlayInteractor() {
         underTest =
             UdfpsOverlayInteractor(
+                context,
                 authController,
                 selectedUserInteractor,
                 testScope.backgroundScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index b4ae00d..42d2c98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -217,6 +217,7 @@
 
         deviceEntrySideFpsOverlayInteractor =
             DeviceEntrySideFpsOverlayInteractor(
+                testScope.backgroundScope,
                 mContext,
                 deviceEntryFingerprintAuthRepository,
                 primaryBouncerInteractor,
@@ -260,14 +261,14 @@
             SideFpsOverlayViewBinder(
                 testScope.backgroundScope,
                 mContext,
-                biometricStatusInteractor,
-                displayStateInteractor,
-                deviceEntrySideFpsOverlayInteractor,
-                fpsUnlockTracker,
-                layoutInflater,
-                sideFpsProgressBarViewModel,
-                sfpsSensorInteractor,
-                windowManager
+                { biometricStatusInteractor },
+                { displayStateInteractor },
+                { deviceEntrySideFpsOverlayInteractor },
+                { fpsUnlockTracker },
+                { layoutInflater },
+                { sideFpsProgressBarViewModel },
+                { sfpsSensorInteractor },
+                { windowManager }
             )
 
         context.addMockSystemService(DisplayManager::class.java, displayManager)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt
index 863d9eb..1b6aaab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt
@@ -18,20 +18,17 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
-import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModelTransitionsMock
+import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.systemUIDialogManager
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -47,24 +44,14 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class DeviceEntryUdfpsTouchOverlayViewModelTest : SysuiTestCase() {
-    val kosmos =
+    private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
         }
-    val testScope = kosmos.testScope
-
-    private val testDeviceEntryIconTransitionAlpha = MutableStateFlow(0f)
-    private val testDeviceEntryIconTransition: DeviceEntryIconTransition
-        get() =
-            object : DeviceEntryIconTransition {
-                override val deviceEntryParentViewAlpha: Flow<Float> =
-                    testDeviceEntryIconTransitionAlpha.asStateFlow()
-            }
-
-    init {
-        kosmos.deviceEntryIconViewModelTransitionsMock.add(testDeviceEntryIconTransition)
-    }
-    val systemUIDialogManager = kosmos.systemUIDialogManager
+    private val systemUIDialogManager = kosmos.systemUIDialogManager
+    private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+    private val testScope = kosmos.testScope
+    private val deviceEntryIconViewModelTransition = kosmos.fakeDeviceEntryIconViewModelTransition
     private val underTest = kosmos.deviceEntryUdfpsTouchOverlayViewModel
 
     @Captor
@@ -80,7 +67,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = 1f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(1f)
             runCurrent()
 
             verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture())
@@ -94,7 +81,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = .3f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(.3f)
             runCurrent()
 
             verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture())
@@ -108,7 +95,7 @@
         testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
 
-            testDeviceEntryIconTransitionAlpha.value = 1f
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(1f)
             runCurrent()
 
             verify(systemUIDialogManager).registerListener(sysuiDialogListenerCaptor.capture())
@@ -116,4 +103,16 @@
 
             assertThat(shouldHandleTouches).isTrue()
         }
+
+    @Test
+    fun deviceEntryViewAlphaZero_alternateBouncerVisible_shouldHandleTouchesTrue() =
+        testScope.runTest {
+            val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
+
+            deviceEntryIconViewModelTransition.setDeviceEntryParentViewAlpha(0f)
+            runCurrent()
+
+            bouncerRepository.setAlternateVisible(true)
+            assertThat(shouldHandleTouches).isTrue()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 7475235..6170e0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -128,6 +128,7 @@
             )
         udfpsOverlayInteractor =
             UdfpsOverlayInteractor(
+                context,
                 authController,
                 selectedUserInteractor,
                 testScope.backgroundScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 2267bdc..983e4b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -220,6 +220,7 @@
 
         deviceEntrySideFpsOverlayInteractor =
             DeviceEntrySideFpsOverlayInteractor(
+                testScope.backgroundScope,
                 mContext,
                 deviceEntryFingerprintAuthRepository,
                 primaryBouncerInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt
deleted file mode 100644
index bfa3641..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt
+++ /dev/null
@@ -1,138 +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.common.domain.interactor
-
-import android.content.res.Configuration
-import android.graphics.Rect
-import android.testing.AndroidTestingRunner
-import android.view.Surface.ROTATION_0
-import android.view.Surface.ROTATION_90
-import android.view.Surface.Rotation
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
-import com.android.systemui.coroutines.collectValues
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-open class ConfigurationInteractorTest : SysuiTestCase() {
-
-    private val testScope = TestScope()
-
-    private val configurationController = FakeConfigurationController()
-    private val configurationRepository =
-        ConfigurationRepositoryImpl(
-            configurationController,
-            context,
-            testScope.backgroundScope,
-            mock()
-        )
-
-    private lateinit var configuration: Configuration
-    private lateinit var underTest: ConfigurationInteractor
-
-    @Before
-    fun setUp() {
-        configuration = context.resources.configuration
-
-        val testableResources = context.getOrCreateTestableResources()
-        testableResources.overrideConfiguration(configuration)
-
-        underTest = ConfigurationInteractorImpl(configurationRepository)
-    }
-
-    @Test
-    fun maxBoundsChange_emitsMaxBoundsChange() =
-        testScope.runTest {
-            val values by collectValues(underTest.naturalMaxBounds)
-
-            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
-            runCurrent()
-            updateDisplay(width = DISPLAY_WIDTH * 2, height = DISPLAY_HEIGHT * 3)
-            runCurrent()
-
-            assertThat(values)
-                .containsExactly(
-                    Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT),
-                    Rect(0, 0, DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 3),
-                )
-                .inOrder()
-        }
-
-    @Test
-    fun maxBoundsSameOnConfigChange_doesNotEmitMaxBoundsChange() =
-        testScope.runTest {
-            val values by collectValues(underTest.naturalMaxBounds)
-
-            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
-            runCurrent()
-            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
-            runCurrent()
-
-            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
-        }
-
-    @Test
-    fun firstMaxBoundsChange_emitsMaxBoundsChange() =
-        testScope.runTest {
-            val values by collectValues(underTest.naturalMaxBounds)
-
-            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
-            runCurrent()
-
-            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
-        }
-
-    @Test
-    fun displayRotatedButMaxBoundsTheSame_doesNotEmitNewMaxBoundsChange() =
-        testScope.runTest {
-            val values by collectValues(underTest.naturalMaxBounds)
-
-            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
-            runCurrent()
-            updateDisplay(width = DISPLAY_HEIGHT, height = DISPLAY_WIDTH, rotation = ROTATION_90)
-            runCurrent()
-
-            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
-        }
-
-    private fun updateDisplay(
-        width: Int = DISPLAY_WIDTH,
-        height: Int = DISPLAY_HEIGHT,
-        @Rotation rotation: Int = ROTATION_0
-    ) {
-        configuration.windowConfiguration.maxBounds.set(Rect(0, 0, width, height))
-        configuration.windowConfiguration.displayRotation = rotation
-
-        configurationController.onConfigurationChanged(configuration)
-    }
-
-    private companion object {
-        private const val DISPLAY_WIDTH = 100
-        private const val DISPLAY_HEIGHT = 200
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt
index c5c0208..9e007e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt
@@ -16,14 +16,19 @@
 
 package com.android.systemui.common.ui.domain.interactor
 
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.view.Surface
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -35,13 +40,16 @@
 @RunWith(AndroidJUnit4::class)
 class ConfigurationInteractorTest : SysuiTestCase() {
     private lateinit var testScope: TestScope
+    private lateinit var configuration: Configuration
     private lateinit var underTest: ConfigurationInteractor
     private lateinit var configurationRepository: FakeConfigurationRepository
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
+        configuration = context.resources.configuration
+        val testableResources = context.getOrCreateTestableResources()
+        testableResources.overrideConfiguration(configuration)
         configurationRepository = FakeConfigurationRepository()
         testScope = TestScope()
         underTest = ConfigurationInteractor(configurationRepository)
@@ -79,4 +87,79 @@
             assertThat(dimensionPixelSizes!![resourceId1]).isEqualTo(pixelSize1)
             assertThat(dimensionPixelSizes!![resourceId2]).isEqualTo(pixelSize2)
         }
+
+    @Test
+    fun maxBoundsChange_emitsMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+            updateDisplay(width = DISPLAY_WIDTH * 2, height = DISPLAY_HEIGHT * 3)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT),
+                    Rect(0, 0, DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 3),
+                )
+                .inOrder()
+        }
+
+    @Test
+    fun maxBoundsSameOnConfigChange_doesNotEmitMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+
+            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+        }
+
+    @Test
+    fun firstMaxBoundsChange_emitsMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+
+            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+        }
+
+    @Test
+    fun displayRotatedButMaxBoundsTheSame_doesNotEmitNewMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+            updateDisplay(
+                width = DISPLAY_HEIGHT,
+                height = DISPLAY_WIDTH,
+                rotation = Surface.ROTATION_90
+            )
+            runCurrent()
+
+            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+        }
+
+    private fun updateDisplay(
+        width: Int = DISPLAY_WIDTH,
+        height: Int = DISPLAY_HEIGHT,
+        @Surface.Rotation rotation: Int = Surface.ROTATION_0
+    ) {
+        configuration.windowConfiguration.maxBounds.set(Rect(0, 0, width, height))
+        configuration.windowConfiguration.displayRotation = rotation
+
+        configurationRepository.onConfigurationChange(configuration)
+    }
+
+    private companion object {
+        private const val DISPLAY_WIDTH = 100
+        private const val DISPLAY_HEIGHT = 200
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index 16b2ed6..9c5cd71 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -140,21 +140,76 @@
             }
             assertThat(widgets())
                 .containsExactly(
+                    communalItemRankEntry2,
+                    communalWidgetItemEntry2,
                     communalItemRankEntry1,
                     communalWidgetItemEntry1,
-                    communalItemRankEntry2,
+                )
+                .inOrder()
+
+            // swapped priorities
+            val widgetIdsToPriorityMap = mapOf(widgetInfo1.widgetId to 2, widgetInfo2.widgetId to 1)
+            communalWidgetDao.updateWidgetOrder(widgetIdsToPriorityMap)
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry1.copy(rank = 2),
+                    communalWidgetItemEntry1,
+                    communalItemRankEntry2.copy(rank = 1),
                     communalWidgetItemEntry2
                 )
+                .inOrder()
+        }
 
-            val widgetIdsInNewOrder = listOf(widgetInfo2.widgetId, widgetInfo1.widgetId)
-            communalWidgetDao.updateWidgetOrder(widgetIdsInNewOrder)
+    @Test
+    fun addNewWidgetWithReorder_emitsWidgetsInNewOrder() =
+        testScope.runTest {
+            val existingWidgets = listOf(widgetInfo1, widgetInfo2)
+            val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+            existingWidgets.forEach {
+                val (widgetId, provider, priority) = it
+                communalWidgetDao.addWidget(
+                    widgetId = widgetId,
+                    provider = provider,
+                    priority = priority,
+                )
+            }
             assertThat(widgets())
                 .containsExactly(
                     communalItemRankEntry2,
                     communalWidgetItemEntry2,
                     communalItemRankEntry1,
-                    communalWidgetItemEntry1
+                    communalWidgetItemEntry1,
                 )
+                .inOrder()
+
+            // map with no item in the middle at index 1
+            val widgetIdsToIndexMap = mapOf(widgetInfo1.widgetId to 1, widgetInfo2.widgetId to 3)
+            communalWidgetDao.updateWidgetOrder(widgetIdsToIndexMap)
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry2.copy(rank = 3),
+                    communalWidgetItemEntry2,
+                    communalItemRankEntry1.copy(rank = 1),
+                    communalWidgetItemEntry1,
+                )
+                .inOrder()
+            // add the new middle item that we left space for.
+            communalWidgetDao.addWidget(
+                widgetId = widgetInfo3.widgetId,
+                provider = widgetInfo3.provider,
+                priority = 2,
+            )
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry2.copy(rank = 3),
+                    communalWidgetItemEntry2,
+                    communalItemRankEntry3.copy(rank = 2),
+                    communalWidgetItemEntry3,
+                    communalItemRankEntry1.copy(rank = 1),
+                    communalWidgetItemEntry1,
+                )
+                .inOrder()
         }
 
     data class FakeWidgetMetadata(
@@ -176,8 +231,15 @@
                 provider = ComponentName("pk_name", "cls_name_2"),
                 priority = 2
             )
+        val widgetInfo3 =
+            FakeWidgetMetadata(
+                widgetId = 3,
+                provider = ComponentName("pk_name", "cls_name_3"),
+                priority = 3
+            )
         val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority)
         val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority)
+        val communalItemRankEntry3 = CommunalItemRank(uid = 3L, rank = widgetInfo3.priority)
         val communalWidgetItemEntry1 =
             CommunalWidgetItem(
                 uid = 1L,
@@ -192,5 +254,12 @@
                 componentName = widgetInfo2.provider.flattenToString(),
                 itemId = communalItemRankEntry2.uid,
             )
+        val communalWidgetItemEntry3 =
+            CommunalWidgetItem(
+                uid = 3L,
+                widgetId = widgetInfo3.widgetId,
+                componentName = widgetInfo3.provider.flattenToString(),
+                itemId = communalItemRankEntry3.uid,
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
new file mode 100644
index 0000000..93ce86a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.deviceentry.domain.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.data.ui.viewmodel.udfpsAccessibilityOverlayViewModel
+import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
+        }
+    private val deviceEntryIconTransition = kosmos.fakeDeviceEntryIconViewModelTransition
+    private val testScope = kosmos.testScope
+    private val accessibilityRepository = kosmos.fakeAccessibilityRepository
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+    private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
+    private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
+    private val shadeRepository = kosmos.fakeShadeRepository
+    private val underTest = kosmos.udfpsAccessibilityOverlayViewModel
+
+    @Test
+    fun visible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            assertThat(visible).isTrue()
+        }
+
+    @Test
+    fun touchExplorationNotEnabled_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            accessibilityRepository.isTouchExplorationEnabled.value = false
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun deviceEntryFgIconViewModelAod_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+
+            // AOD
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                )
+            )
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                )
+            )
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun deviceUnlocked_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            deviceEntryRepository.setUnlocked(true)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun deviceEntryViewAlpha0_overlayNotVisible() =
+        testScope.runTest {
+            val visible by collectLastValue(underTest.visible)
+            setupVisibleStateOnLockscreen()
+            deviceEntryIconTransition.setDeviceEntryParentViewAlpha(0f)
+            assertThat(visible).isFalse()
+        }
+
+    private suspend fun setupVisibleStateOnLockscreen() {
+        // A11y enabled
+        accessibilityRepository.isTouchExplorationEnabled.value = true
+
+        // Transition alpha is 1f
+        deviceEntryIconTransition.setDeviceEntryParentViewAlpha(1f)
+
+        // Listening for UDFPS
+        fingerprintPropertyRepository.supportsUdfps()
+        deviceEntryFingerprintAuthRepository.setIsRunning(true)
+        deviceEntryRepository.setUnlocked(false)
+
+        // Lockscreen
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 0f,
+                transitionState = TransitionState.STARTED,
+            )
+        )
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 1f,
+                transitionState = TransitionState.FINISHED,
+            )
+        )
+
+        // Shade not expanded
+        shadeRepository.qsExpansion.value = 0f
+        shadeRepository.lockscreenShadeExpansion.value = 0f
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
index 70d3f81..027dfa1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.os.Handler
-import android.platform.test.annotations.RequiresFlagsEnabled
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -58,7 +57,6 @@
 import org.mockito.junit.MockitoRule
 
 @OptIn(ExperimentalCoroutinesApi::class)
-@RequiresFlagsEnabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
 @SmallTest
 @RunWith(JUnit4::class)
 class DeviceEntrySideFpsOverlayInteractorTest : SysuiTestCase() {
@@ -80,6 +78,7 @@
 
     @Before
     fun setup() {
+        mSetFlagsRule.enableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         primaryBouncerInteractor =
             PrimaryBouncerInteractor(
                 bouncerRepository,
@@ -110,6 +109,7 @@
             )
         underTest =
             DeviceEntrySideFpsOverlayInteractor(
+                testScope.backgroundScope,
                 mContext,
                 FakeDeviceEntryFingerprintAuthRepository(),
                 primaryBouncerInteractor,
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 3109e76..ad86ee9 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
@@ -37,6 +37,7 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.util.mockito.whenever
@@ -70,7 +71,8 @@
     @Mock private lateinit var communalTutorialIndicatorSection: CommunalTutorialIndicatorSection
     @Mock private lateinit var clockSection: ClockSection
     @Mock private lateinit var smartspaceSection: SmartspaceSection
-
+    @Mock
+    private lateinit var udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -90,6 +92,7 @@
                 communalTutorialIndicatorSection,
                 clockSection,
                 smartspaceSection,
+                udfpsAccessibilityOverlaySection,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index e89b61f..dc0d9c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -20,7 +20,6 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
@@ -43,7 +42,6 @@
     @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel
     @Mock private lateinit var smartspaceViewModel: KeyguardSmartspaceViewModel
     @Mock private lateinit var splitShadeStateController: SplitShadeStateController
-    private var featureFlags: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic()
 
     private lateinit var underTest: ClockSection
 
@@ -88,7 +86,6 @@
                 smartspaceViewModel,
                 mContext,
                 splitShadeStateController,
-                featureFlags
             )
     }
 
@@ -147,24 +144,6 @@
         assetSmallClockTop(cs, expectedSmallClockTopMargin)
     }
 
-    @Test
-    fun testLargeClockShouldBeCentered() {
-        underTest.setClockShouldBeCentered(true)
-        val cs = ConstraintSet()
-        underTest.applyDefaultConstraints(cs)
-        val constraint = cs.getConstraint(R.id.lockscreen_clock_view_large)
-        assertThat(constraint.layout.endToEnd).isEqualTo(ConstraintSet.PARENT_ID)
-    }
-
-    @Test
-    fun testLargeClockShouldNotBeCentered() {
-        underTest.setClockShouldBeCentered(false)
-        val cs = ConstraintSet()
-        underTest.applyDefaultConstraints(cs)
-        val constraint = cs.getConstraint(R.id.lockscreen_clock_view_large)
-        assertThat(constraint.layout.endToEnd).isEqualTo(R.id.split_shade_guideline)
-    }
-
     private fun setLargeClock(useLargeClock: Boolean) {
         whenever(keyguardClockViewModel.useLargeClock).thenReturn(useLargeClock)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index 22569e2..c864704 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -30,20 +30,15 @@
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
-import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
 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.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.gesture.TapGestureDetector
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -87,11 +82,6 @@
                 { mock(DeviceEntryForegroundViewModel::class.java) },
                 { mock(DeviceEntryBackgroundViewModel::class.java) },
                 { falsingManager },
-                { mock(AlternateBouncerViewModel::class.java) },
-                { mock(NotificationShadeWindowController::class.java) },
-                TestScope().backgroundScope,
-                { mock(SwipeUpAnywhereGestureHandler::class.java) },
-                { mock(TapGestureDetector::class.java) },
                 { mock(VibratorHelper::class.java) },
             )
     }
@@ -177,21 +167,4 @@
         assertThat(constraint.layout.topMargin).isEqualTo(5)
         assertThat(constraint.layout.startMargin).isEqualTo(4)
     }
-
-    @Test
-    fun deviceEntryIconViewIsAboveAlternateBouncerView() {
-        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
-
-        val constraintLayout = ConstraintLayout(context, null)
-        underTest.addViews(constraintLayout)
-        assertThat(constraintLayout.childCount).isGreaterThan(0)
-        val deviceEntryIconView = constraintLayout.getViewById(R.id.device_entry_icon_view)
-        val alternateBouncerView = constraintLayout.getViewById(R.id.alternate_bouncer)
-        assertThat(deviceEntryIconView).isNotNull()
-        assertThat(alternateBouncerView).isNotNull()
-
-        // device entry icon is above the alternate bouncer
-        assertThat(constraintLayout.indexOfChild(deviceEntryIconView))
-            .isGreaterThan(constraintLayout.indexOfChild(alternateBouncerView))
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
index bff27f6..740110b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
@@ -69,14 +69,13 @@
                 keyguardUnlockAnimationController,
             )
         constraintLayout = ConstraintLayout(mContext)
-        whenever(lockscreenSmartspaceController.buildAndConnectView(constraintLayout))
-            .thenReturn(smartspaceView)
-        whenever(lockscreenSmartspaceController.buildAndConnectWeatherView(constraintLayout))
-            .thenReturn(weatherView)
-        whenever(lockscreenSmartspaceController.buildAndConnectDateView(constraintLayout))
-            .thenReturn(dateView)
+        whenever(keyguardSmartspaceViewModel.smartspaceView).thenReturn(smartspaceView)
+        whenever(keyguardSmartspaceViewModel.weatherView).thenReturn(weatherView)
+        whenever(keyguardSmartspaceViewModel.dateView).thenReturn(dateView)
         whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay)
             .thenReturn(hasCustomWeatherDataDisplay)
+        whenever(keyguardSmartspaceViewModel.smartspaceController)
+            .thenReturn(lockscreenSmartspaceController)
         constraintSet = ConstraintSet()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 459a74c..ee1be10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.collectLastValue
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.coroutines.collectLastValue
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
deleted file mode 100644
index 6512290..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
+++ /dev/null
@@ -1,139 +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.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@ExperimentalCoroutinesApi
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsAodViewModelTest : SysuiTestCase() {
-    private val defaultPadding = 12
-    private lateinit var underTest: UdfpsAodViewModel
-
-    private lateinit var testScope: TestScope
-    private lateinit var configRepository: FakeConfigurationRepository
-    private lateinit var bouncerRepository: KeyguardBouncerRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var shadeRepository: FakeShadeRepository
-    private lateinit var keyguardInteractor: KeyguardInteractor
-
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding)
-        testScope = TestScope()
-        shadeRepository = FakeShadeRepository()
-        KeyguardInteractorFactory.create().also {
-            keyguardInteractor = it.keyguardInteractor
-            keyguardRepository = it.repository
-            configRepository = it.configurationRepository
-            bouncerRepository = it.bouncerRepository
-        }
-        val udfpsKeyguardInteractor =
-            UdfpsKeyguardInteractor(
-                configRepository,
-                BurnInInteractor(
-                    context,
-                    burnInHelper,
-                    testScope.backgroundScope,
-                    configRepository,
-                    keyguardInteractor,
-                ),
-                keyguardInteractor,
-                shadeRepository,
-                dialogManager,
-            )
-
-        underTest =
-            UdfpsAodViewModel(
-                udfpsKeyguardInteractor,
-                context,
-            )
-    }
-
-    @Test
-    fun alphaAndVisibleUpdates_onDozeAmountChanges() =
-        testScope.runTest {
-            val alpha by collectLastValue(underTest.alpha)
-            val visible by collectLastValue(underTest.isVisible)
-
-            keyguardRepository.setDozeAmount(0f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            keyguardRepository.setDozeAmount(.65f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(.65f)
-            assertThat(visible).isTrue()
-
-            keyguardRepository.setDozeAmount(.23f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(.23f)
-            assertThat(visible).isTrue()
-
-            keyguardRepository.setDozeAmount(1f)
-            runCurrent()
-            assertThat(alpha).isEqualTo(1f)
-            assertThat(visible).isTrue()
-        }
-
-    @Test
-    fun paddingUpdates_onScaleForResolutionChanges() =
-        testScope.runTest {
-            val padding by collectLastValue(underTest.padding)
-
-            configRepository.setScaleForResolution(1f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding)
-
-            configRepository.setScaleForResolution(2f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding * 2)
-
-            configRepository.setScaleForResolution(.5f)
-            runCurrent()
-            assertThat(padding).isEqualTo((defaultPadding * .5f).toInt())
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
deleted file mode 100644
index 95b2fe5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
+++ /dev/null
@@ -1,130 +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.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.doze.util.BurnInHelperWrapper
-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.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-/** Tests UdfpsFingerprintViewModel specific flows. */
-@ExperimentalCoroutinesApi
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsFingerprintViewModelTest : SysuiTestCase() {
-    private val defaultPadding = 12
-    private lateinit var underTest: FingerprintViewModel
-
-    private lateinit var testScope: TestScope
-    private lateinit var configRepository: FakeConfigurationRepository
-    private lateinit var bouncerRepository: KeyguardBouncerRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var fakeCommandQueue: FakeCommandQueue
-    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var shadeRepository: FakeShadeRepository
-
-    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding)
-        testScope = TestScope()
-        configRepository = FakeConfigurationRepository()
-        keyguardRepository = FakeKeyguardRepository()
-        bouncerRepository = FakeKeyguardBouncerRepository()
-        fakeCommandQueue = FakeCommandQueue()
-        bouncerRepository = FakeKeyguardBouncerRepository()
-        transitionRepository = FakeKeyguardTransitionRepository()
-        shadeRepository = FakeShadeRepository()
-        val keyguardInteractor =
-            KeyguardInteractorFactory.create(
-                    repository = keyguardRepository,
-                )
-                .keyguardInteractor
-
-        val transitionInteractor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = testScope.backgroundScope,
-                    repository = transitionRepository,
-                    keyguardInteractor = keyguardInteractor,
-                )
-                .keyguardTransitionInteractor
-
-        underTest =
-            FingerprintViewModel(
-                context,
-                transitionInteractor,
-                UdfpsKeyguardInteractor(
-                    configRepository,
-                    BurnInInteractor(
-                        context,
-                        burnInHelper,
-                        testScope.backgroundScope,
-                        configRepository,
-                        keyguardInteractor,
-                    ),
-                    keyguardInteractor,
-                    shadeRepository,
-                    dialogManager,
-                ),
-                keyguardInteractor,
-            )
-    }
-
-    @Test
-    fun paddingUpdates_onScaleForResolutionChanges() =
-        testScope.runTest {
-            val padding by collectLastValue(underTest.padding)
-
-            configRepository.setScaleForResolution(1f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding)
-
-            configRepository.setScaleForResolution(2f)
-            runCurrent()
-            assertThat(padding).isEqualTo(defaultPadding * 2)
-
-            configRepository.setScaleForResolution(.5f)
-            runCurrent()
-            assertThat(padding).isEqualTo((defaultPadding * .5).toInt())
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
deleted file mode 100644
index 848a94b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ /dev/null
@@ -1,749 +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.keyguard.ui.viewmodel
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.settingslib.Utils
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.wm.shell.animation.Interpolators
-import com.google.common.collect.Range
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.MockitoAnnotations
-
-/** Tests UDFPS lockscreen view model transitions. */
-@ExperimentalCoroutinesApi
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsLockscreenViewModelTest : SysuiTestCase() {
-    private val lockscreenColorResId = android.R.attr.textColorPrimary
-    private val alternateBouncerResId = com.android.internal.R.attr.materialColorOnPrimaryFixed
-    private val lockscreenColor = Utils.getColorAttrDefaultColor(context, lockscreenColorResId)
-    private val alternateBouncerColor =
-        Utils.getColorAttrDefaultColor(context, alternateBouncerResId)
-
-    @Mock private lateinit var dialogManager: SystemUIDialogManager
-
-    private lateinit var underTest: UdfpsLockscreenViewModel
-    private lateinit var testScope: TestScope
-    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var configRepository: FakeConfigurationRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-    private lateinit var keyguardInteractor: KeyguardInteractor
-    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
-    private lateinit var shadeRepository: FakeShadeRepository
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        testScope = TestScope()
-        transitionRepository = FakeKeyguardTransitionRepository()
-        shadeRepository = FakeShadeRepository()
-        KeyguardInteractorFactory.create().also {
-            keyguardInteractor = it.keyguardInteractor
-            keyguardRepository = it.repository
-            configRepository = it.configurationRepository
-            bouncerRepository = it.bouncerRepository
-        }
-
-        val transitionInteractor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = testScope.backgroundScope,
-                    repository = transitionRepository,
-                    keyguardInteractor = keyguardInteractor,
-                )
-                .keyguardTransitionInteractor
-
-        underTest =
-            UdfpsLockscreenViewModel(
-                context,
-                lockscreenColorResId,
-                alternateBouncerResId,
-                transitionInteractor,
-                UdfpsKeyguardInteractor(
-                    configRepository,
-                    BurnInInteractor(
-                        context,
-                        burnInHelperWrapper = mock(),
-                        testScope.backgroundScope,
-                        configRepository,
-                        keyguardInteractor,
-                    ),
-                    keyguardInteractor,
-                    shadeRepository,
-                    dialogManager,
-                ),
-                keyguardInteractor,
-            )
-    }
-
-    @Test
-    fun goneToAodTransition() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: gone -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "goneToAodTransition",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.RUNNING: gone -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "goneToAodTransition",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.FINISHED: gone -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "goneToAodTransition",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun lockscreenToAod() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            // TransitionState.STARTED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun lockscreenShadeLockedToAod() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
-
-            // TransitionState.STARTED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.RUNNING: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-
-            // TransitionState.FINISHED: lockscreen -> AOD
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun aodToLockscreen() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: AOD -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "aodToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isFalse()
-
-            // TransitionState.RUNNING: AOD -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "aodToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: AOD -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "aodToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-        }
-
-    @Test
-    fun lockscreenToAlternateBouncer() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            // TransitionState.STARTED: lockscreen -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: lockscreen -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: lockscreen -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-        }
-
-    fun alternateBouncerToPrimaryBouncer() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: alternate bouncer -> primary bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "alternateBouncerToPrimaryBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: alternate bouncer -> primary bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "alternateBouncerToPrimaryBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: alternate bouncer -> primary bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.PRIMARY_BOUNCER,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "alternateBouncerToPrimaryBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isFalse()
-        }
-
-    fun alternateBouncerToAod() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: alternate bouncer -> aod
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.AOD,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "alternateBouncerToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: alternate bouncer -> aod
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.AOD,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "alternateBouncerToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: alternate bouncer -> aod
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.ALTERNATE_BOUNCER,
-                    to = KeyguardState.AOD,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "alternateBouncerToAod",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun lockscreenToOccluded() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            // TransitionState.STARTED: lockscreen -> occluded
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.OCCLUDED,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "lockscreenToOccluded",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: lockscreen -> occluded
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.OCCLUDED,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "lockscreenToOccluded",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: lockscreen -> occluded
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.OCCLUDED,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "lockscreenToOccluded",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isFalse()
-        }
-
-    @Test
-    fun occludedToLockscreen() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: occluded -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "occludedToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: occluded -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "occludedToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: occluded -> lockscreen
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.LOCKSCREEN,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "occludedToLockscreen",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(lockscreenColor)
-            assertThat(visible).isTrue()
-        }
-
-    @Test
-    fun qsProgressChange() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            givenTransitionToLockscreenFinished()
-
-            // qsExpansion = 0f
-            shadeRepository.setQsExpansion(0f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(visible).isEqualTo(true)
-
-            // qsExpansion = .25
-            shadeRepository.setQsExpansion(.2f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(.6f)
-            assertThat(visible).isEqualTo(true)
-
-            // qsExpansion = .5
-            shadeRepository.setQsExpansion(.5f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isEqualTo(false)
-
-            // qsExpansion = 1
-            shadeRepository.setQsExpansion(1f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isEqualTo(false)
-        }
-
-    @Test
-    fun shadeExpansionChanged() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-            givenTransitionToLockscreenFinished()
-
-            // shadeExpansion = 0f
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(0f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(visible).isEqualTo(true)
-
-            // shadeExpansion = .2
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(.2f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(.8f)
-            assertThat(visible).isEqualTo(true)
-
-            // shadeExpansion = .5
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(.5f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(.5f)
-            assertThat(visible).isEqualTo(true)
-
-            // shadeExpansion = 1
-            shadeRepository.setUdfpsTransitionToFullShadeProgress(1f)
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(0f)
-            assertThat(visible).isEqualTo(false)
-        }
-
-    @Test
-    fun dialogHideAffordancesRequestChanged() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            givenTransitionToLockscreenFinished()
-            runCurrent()
-            val captor = argumentCaptor<SystemUIDialogManager.Listener>()
-            Mockito.verify(dialogManager).registerListener(captor.capture())
-
-            captor.value.shouldHideAffordances(true)
-            assertThat(transition?.alpha).isEqualTo(0f)
-
-            captor.value.shouldHideAffordances(false)
-            assertThat(transition?.alpha).isEqualTo(1f)
-        }
-
-    @Test
-    fun occludedToAlternateBouncer() =
-        testScope.runTest {
-            val transition by collectLastValue(underTest.transition)
-            val visible by collectLastValue(underTest.visible)
-
-            // TransitionState.STARTED: occluded -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 0f,
-                    transitionState = TransitionState.STARTED,
-                    ownerName = "occludedToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(0f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.RUNNING: occluded -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = .6f,
-                    transitionState = TransitionState.RUNNING,
-                    ownerName = "occludedToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale)
-                .isEqualTo(Interpolators.FAST_OUT_SLOW_IN.getInterpolation(.6f))
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-
-            // TransitionState.FINISHED: occluded -> alternate bouncer
-            transitionRepository.sendTransitionStep(
-                TransitionStep(
-                    from = KeyguardState.OCCLUDED,
-                    to = KeyguardState.ALTERNATE_BOUNCER,
-                    value = 1f,
-                    transitionState = TransitionState.FINISHED,
-                    ownerName = "occludedToAlternateBouncer",
-                )
-            )
-            runCurrent()
-            assertThat(transition?.alpha).isEqualTo(1f)
-            assertThat(transition?.scale).isEqualTo(1f)
-            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
-            assertThat(visible).isTrue()
-        }
-
-    private suspend fun givenTransitionToLockscreenFinished() {
-        transitionRepository.sendTransitionSteps(
-            from = KeyguardState.AOD,
-            to = KeyguardState.LOCKSCREEN,
-            testScope
-        )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
index 8532ffe..94b9fa4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
@@ -55,6 +55,7 @@
 private const val KEY_ALT = "TEST_KEY_2"
 private const val USER_MAIN = 0
 private const val USER_GUEST = 10
+private const val PRIVATE_PROFILE = 12
 private const val PACKAGE = "PKG"
 private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
 private const val APP_UID = 99
@@ -82,6 +83,7 @@
     private lateinit var mediaDataFilter: MediaDataFilter
     private lateinit var dataMain: MediaData
     private lateinit var dataGuest: MediaData
+    private lateinit var dataPrivateProfile: MediaData
     private val clock = FakeSystemClock()
 
     @Before
@@ -115,6 +117,7 @@
                 appUid = APP_UID
             )
         dataGuest = dataMain.copy(userId = USER_GUEST)
+        dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE)
 
         whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
         whenever(smartspaceData.isActive).thenReturn(true)
@@ -130,8 +133,19 @@
 
     private fun setUser(id: Int) {
         whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+        whenever(lockscreenUserManager.isProfileAvailable(anyInt())).thenReturn(false)
         whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
-        mediaDataFilter.handleUserSwitched(id)
+        whenever(lockscreenUserManager.isProfileAvailable(eq(id))).thenReturn(true)
+        whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(true)
+        mediaDataFilter.handleUserSwitched()
+    }
+
+    private fun setPrivateProfileUnavailable() {
+        whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+        whenever(lockscreenUserManager.isCurrentProfile(eq(USER_MAIN))).thenReturn(true)
+        whenever(lockscreenUserManager.isCurrentProfile(eq(PRIVATE_PROFILE))).thenReturn(true)
+        whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(false)
+        mediaDataFilter.handleProfileChanged()
     }
 
     @Test
@@ -206,6 +220,20 @@
     }
 
     @Test
+    fun testOnProfileChanged_profileUnavailable_loadControls() {
+        // GIVEN that we had some media for both profiles
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile)
+        reset(listener)
+
+        // and we change profile status
+        setPrivateProfileUnavailable()
+
+        // THEN we should add the private profile media
+        verify(listener).onMediaDataRemoved(eq(KEY_ALT))
+    }
+
+    @Test
     fun hasAnyMedia_noMediaSet_returnsFalse() {
         assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 1f99303..0a464e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -22,11 +22,11 @@
 import android.testing.TestableLooper
 import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.models.player.MediaViewHolder
 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
 import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.res.R
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionViewState
@@ -37,6 +37,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.floatThat
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -183,10 +184,12 @@
         // detail widgets occupy [90, 100]
         whenever(detailWidgetState.y).thenReturn(90F)
         whenever(detailWidgetState.height).thenReturn(10)
+        whenever(detailWidgetState.alpha).thenReturn(1F)
         // control widgets occupy [150, 170]
         whenever(controlWidgetState.y).thenReturn(150F)
         whenever(controlWidgetState.height).thenReturn(20)
-        // in current beizer, when the progress reach 0.38, the result will be 0.5
+        whenever(controlWidgetState.alpha).thenReturn(1F)
+        // in current bezier, when the progress reach 0.38, the result will be 0.5
         mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
         verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
         verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
@@ -196,6 +199,34 @@
     }
 
     @Test
+    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_invisibleElements() {
+        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+        whenever(mockCopiedState.widgetStates)
+            .thenReturn(
+                mutableMapOf(
+                    R.id.media_progress_bar to controlWidgetState,
+                    R.id.header_artist to detailWidgetState
+                )
+            )
+        whenever(mockCopiedState.measureHeight).thenReturn(200)
+        // detail widgets occupy [90, 100]
+        whenever(detailWidgetState.y).thenReturn(90F)
+        whenever(detailWidgetState.height).thenReturn(10)
+        whenever(detailWidgetState.alpha).thenReturn(0F)
+        // control widgets occupy [150, 170]
+        whenever(controlWidgetState.y).thenReturn(150F)
+        whenever(controlWidgetState.height).thenReturn(20)
+        whenever(controlWidgetState.alpha).thenReturn(0F)
+        // Verify that alpha remains 0 throughout squishing
+        mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
+        verify(controlWidgetState, never()).alpha = floatThat { it > 0 }
+        verify(detailWidgetState, never()).alpha = floatThat { it > 0 }
+        mediaViewController.squishViewState(mockViewState, 200F / 200F)
+        verify(controlWidgetState, never()).alpha = floatThat { it > 0 }
+        verify(detailWidgetState, never()).alpha = floatThat { it > 0 }
+    }
+
+    @Test
     fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
         whenever(mockViewState.copy()).thenReturn(mockCopiedState)
         whenever(mockCopiedState.widgetStates)
@@ -210,12 +241,15 @@
         // media container widgets occupy [20, 300]
         whenever(mediaContainerWidgetState.y).thenReturn(20F)
         whenever(mediaContainerWidgetState.height).thenReturn(280)
+        whenever(mediaContainerWidgetState.alpha).thenReturn(1F)
         // media title widgets occupy [320, 330]
         whenever(mediaTitleWidgetState.y).thenReturn(320F)
         whenever(mediaTitleWidgetState.height).thenReturn(10)
+        whenever(mediaTitleWidgetState.alpha).thenReturn(1F)
         // media subtitle widgets occupy [340, 350]
         whenever(mediaSubTitleWidgetState.y).thenReturn(340F)
         whenever(mediaSubTitleWidgetState.height).thenReturn(10)
+        whenever(mediaSubTitleWidgetState.alpha).thenReturn(1F)
 
         // in current beizer, when the progress reach 0.38, the result will be 0.5
         mediaViewController.squishViewState(mockViewState, 307.6F / 360F)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
index bcbf666..16c92ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
@@ -18,38 +18,27 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_PSS_TASK_SWITCHER
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.mediaprojection.taskswitcher.ui.TaskSwitcherNotificationCoordinator
-import com.android.systemui.util.mockito.whenever
-import org.junit.Before
+import com.android.systemui.util.mockito.mock
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class MediaProjectionTaskSwitcherCoreStartableTest : SysuiTestCase() {
 
-    @Mock private lateinit var flags: FeatureFlags
-    @Mock private lateinit var coordinator: TaskSwitcherNotificationCoordinator
+    private val coordinator = mock<TaskSwitcherNotificationCoordinator>()
 
-    private lateinit var coreStartable: MediaProjectionTaskSwitcherCoreStartable
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        coreStartable = MediaProjectionTaskSwitcherCoreStartable(coordinator, flags)
-    }
+    private val coreStartable =
+        MediaProjectionTaskSwitcherCoreStartable(notificationCoordinatorLazy = { coordinator })
 
     @Test
     fun start_flagEnabled_startsCoordinator() {
-        whenever(flags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)).thenReturn(true)
+        mSetFlagsRule.enableFlags(FLAG_PSS_TASK_SWITCHER)
 
         coreStartable.start()
 
@@ -58,7 +47,7 @@
 
     @Test
     fun start_flagDisabled_doesNotStartCoordinator() {
-        whenever(flags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)).thenReturn(false)
+        mSetFlagsRule.disableFlags(FLAG_PSS_TASK_SWITCHER)
 
         coreStartable.start()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 4d42324..b7618d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -75,7 +75,6 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
@@ -463,7 +462,6 @@
 
     // region setNoteTaskShortcutEnabled
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_setTrue() {
         createNoteTaskController().setNoteTaskShortcutEnabled(value = true, userTracker.userHandle)
 
@@ -480,7 +478,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_setFalse() {
         createNoteTaskController().setNoteTaskShortcutEnabled(value = false, userTracker.userHandle)
 
@@ -497,7 +494,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_workProfileUser_setTrue() {
         whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any()))
             .thenReturn(workProfileContext)
@@ -519,7 +515,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun setNoteTaskShortcutEnabled_workProfileUser_setFalse() {
         whenever(context.createContextAsUser(eq(workUserInfo.userHandle), any()))
             .thenReturn(workProfileContext)
@@ -738,7 +733,6 @@
 
     // region internalUpdateNoteTaskAsUser
     @Test
-    @Ignore("b/316332684")
     fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
         createNoteTaskController(isEnabled = true)
             .launchUpdateNoteTaskAsUser(userTracker.userHandle)
@@ -772,7 +766,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() {
         whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle))
             .thenReturn(emptyList())
@@ -796,7 +789,6 @@
     }
 
     @Test
-    @Ignore("b/316332684")
     fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() {
         createNoteTaskController(isEnabled = false)
             .launchUpdateNoteTaskAsUser(userTracker.userHandle)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 5e2423a..ef7798e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -447,10 +447,6 @@
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1,spec1");
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "default");
-
-        // Remove spurious tiles, like dbg:mem
-        specs.removeIf(spec -> !"spec1".equals(spec));
-        assertEquals(1, specs.size());
     }
 
     @Test
@@ -458,10 +454,6 @@
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1");
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "default,spec1");
-
-        // Remove spurious tiles, like dbg:mem
-        specs.removeIf(spec -> !"spec1".equals(spec));
-        assertEquals(1, specs.size());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index fbd63c6..8142456 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -300,8 +301,10 @@
         mStateManager.onStartListening();
         mStateManager.executeSetBindService(true);
         mExecutor.runAllReady();
-        mStateManager.onServiceDisconnected(mTileServiceComponentName);
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
         mClock.advanceTime(5000);
+        mExecutor.runAllReady();
 
         // Two calls: one for the first bind, one for the restart.
         verifyBind(2);
@@ -318,20 +321,66 @@
         mStateManager.onStartListening();
         mStateManager.executeSetBindService(true);
         mExecutor.runAllReady();
-        mStateManager.onServiceDisconnected(mTileServiceComponentName);
+        verify(mMockTileService, times(1)).onStartListening();
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
 
         // Longer delay than a regular one
         mClock.advanceTime(5000);
-        verifyBind(1);
-        verify(mMockTileService, times(1)).onStartListening();
+        mExecutor.runAllReady();
+
+        assertFalse(mContext.isBound(mTileServiceComponentName));
 
         mClock.advanceTime(20000);
+        mExecutor.runAllReady();
         // Two calls: one for the first bind, one for the restart.
         verifyBind(2);
         verify(mMockTileService, times(2)).onStartListening();
     }
 
     @Test
+    public void testOnServiceDisconnectedDoesnUnbind_doesntForwardToBinder() throws Exception {
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+
+        mStateManager.onStartListening();
+        verify(mMockTileService).onStartListening();
+
+        clearInvocations(mMockTileService);
+        mStateManager.onServiceDisconnected(mTileServiceComponentName);
+        mExecutor.runAllReady();
+
+        mStateManager.onStartListening();
+        verify(mMockTileService, never()).onStartListening();
+    }
+
+    @Test
+    public void testKillProcessLowMemory_unbound_doesntBindAgain() throws Exception {
+        doAnswer(invocation -> {
+            ActivityManager.MemoryInfo memoryInfo = invocation.getArgument(0);
+            memoryInfo.lowMemory = true;
+            return null;
+        }).when(mActivityManager).getMemoryInfo(any());
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+
+        clearInvocations(mMockTileService);
+        mStateManager.executeSetBindService(false);
+        mExecutor.runAllReady();
+        mClock.advanceTime(30000);
+        mExecutor.runAllReady();
+
+        verifyBind(0);
+        verify(mMockTileService, never()).onStartListening();
+    }
+
+    @Test
     public void testToggleableTile() throws Exception {
         assertTrue(mStateManager.isToggleableTile());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
index 067218a..5201e5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -50,7 +50,6 @@
 import com.android.systemui.qs.tiles.ScreenRecordTile
 import com.android.systemui.qs.tiles.UiModeNightTile
 import com.android.systemui.qs.tiles.WorkModeTile
-import com.android.systemui.util.leak.GarbageMonitor
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -117,7 +116,6 @@
     @Mock private lateinit var dataSaverTile: DataSaverTile
     @Mock private lateinit var nightDisplayTile: NightDisplayTile
     @Mock private lateinit var nfcTile: NfcTile
-    @Mock private lateinit var memoryTile: GarbageMonitor.MemoryTile
     @Mock private lateinit var darkModeTile: UiModeNightTile
     @Mock private lateinit var screenRecordTile: ScreenRecordTile
     @Mock private lateinit var reduceBrightColorsTile: ReduceBrightColorsTile
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
index 9b61447..c7479fd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
 import com.android.systemui.res.R
-import com.android.systemui.settings.UserContextProvider
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -65,9 +65,9 @@
     @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var dialogLauncherAnimator: DialogLaunchAnimator
-    @Mock private lateinit var dialogFactory: SystemUIDialog.Factory
+    @Mock private lateinit var delegateFactory: RecordIssueDialogDelegate.Factory
+    @Mock private lateinit var dialogDelegate: RecordIssueDialogDelegate
     @Mock private lateinit var dialog: SystemUIDialog
-    @Mock private lateinit var userContextProvider: UserContextProvider
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var tile: RecordIssueTile
@@ -76,7 +76,8 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(host.context).thenReturn(mContext)
-        whenever(dialogFactory.create(any())).thenReturn(dialog)
+        whenever(delegateFactory.create(any())).thenReturn(dialogDelegate)
+        whenever(dialogDelegate.createDialog()).thenReturn(dialog)
 
         testableLooper = TestableLooper.get(this)
         tile =
@@ -93,8 +94,7 @@
                 keyguardDismissUtil,
                 keyguardStateController,
                 dialogLauncherAnimator,
-                dialogFactory,
-                userContextProvider,
+                delegateFactory,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index c108a80..273ce85 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -28,8 +28,8 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -82,7 +82,7 @@
         TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
                 R.id.rear_display_title_text_view);
 
-        controller.onConfigurationChanged(new Configuration());
+        controller.onConfigChanged(new Configuration());
         assertTrue(controller.mRearDisplayEducationDialog.isShowing());
         TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById(
                 R.id.rear_display_title_text_view);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index c5d3524..7ce51ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -17,6 +17,9 @@
 package com.android.systemui.recordissue
 
 import android.app.Dialog
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.widget.Button
@@ -25,48 +28,107 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
+import com.android.systemui.mediaprojection.SessionCreationSource
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
 import com.android.systemui.model.SysUiState
+import com.android.systemui.qs.tiles.RecordIssueTile
 import com.android.systemui.res.R
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class RecordIssueDialogDelegateTest : SysuiTestCase() {
 
+    @Mock private lateinit var flags: FeatureFlagsClassic
+    @Mock private lateinit var devicePolicyResolver: ScreenCaptureDevicePolicyResolver
+    @Mock private lateinit var dprLazy: dagger.Lazy<ScreenCaptureDevicePolicyResolver>
+    @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
+    @Mock private lateinit var userContextProvider: UserContextProvider
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var userFileManager: UserFileManager
+    @Mock private lateinit var sharedPreferences: SharedPreferences
+
+    @Mock private lateinit var sysuiState: SysUiState
+    @Mock private lateinit var systemUIDialogManager: SystemUIDialogManager
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var bgExecutor: Executor
+    @Mock private lateinit var mainExecutor: Executor
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
     private lateinit var dialog: SystemUIDialog
+    private lateinit var factory: SystemUIDialog.Factory
     private lateinit var latch: CountDownLatch
 
     @Before
     fun setup() {
-        val dialogFactory =
-            SystemUIDialog.Factory(
-                context,
-                mock<FeatureFlags>(),
-                mock<SystemUIDialogManager>(),
-                mock<SysUiState>().apply {
-                    whenever(setFlag(anyInt(), anyBoolean())).thenReturn(this)
-                },
-                mock<BroadcastDispatcher>(),
-                mock<DialogLaunchAnimator>()
+        MockitoAnnotations.initMocks(this)
+        whenever(dprLazy.get()).thenReturn(devicePolicyResolver)
+        whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
+        whenever(userContextProvider.userContext).thenReturn(mContext)
+        whenever(
+                userFileManager.getSharedPreferences(
+                    eq(RecordIssueTile.TILE_SPEC),
+                    eq(Context.MODE_PRIVATE),
+                    anyInt()
+                )
+            )
+            .thenReturn(sharedPreferences)
+
+        factory =
+            spy(
+                SystemUIDialog.Factory(
+                    context,
+                    flags,
+                    systemUIDialogManager,
+                    sysuiState,
+                    broadcastDispatcher,
+                    dialogLaunchAnimator
+                )
             )
 
         latch = CountDownLatch(1)
         dialog =
-            RecordIssueDialogDelegate(dialogFactory, mock()) { latch.countDown() }.createDialog()
+            RecordIssueDialogDelegate(
+                    factory,
+                    userContextProvider,
+                    userTracker,
+                    flags,
+                    bgExecutor,
+                    mainExecutor,
+                    dprLazy,
+                    mediaProjectionMetricsLogger,
+                    userFileManager,
+                ) {
+                    latch.countDown()
+                }
+                .createDialog()
         dialog.show()
     }
 
@@ -91,4 +153,82 @@
         dialog.getButton(Dialog.BUTTON_POSITIVE).callOnClick()
         latch.await(1L, TimeUnit.MILLISECONDS)
     }
+
+    @Test
+    fun screenCaptureDisabledDialog_isShown_whenFunctionalityIsDisabled() {
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
+            .thenReturn(true)
+        whenever(devicePolicyResolver.isScreenCaptureCompletelyDisabled(any<UserHandle>()))
+            .thenReturn(true)
+
+        val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
+        screenRecordSwitch.isChecked = true
+
+        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(bgExecutor).execute(bgCaptor.capture())
+        bgCaptor.value.run()
+
+        val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(mainExecutor).execute(mainCaptor.capture())
+        mainCaptor.value.run()
+
+        verify(mediaProjectionMetricsLogger, never())
+            .notifyProjectionInitiated(
+                anyInt(),
+                eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
+            )
+        assertThat(screenRecordSwitch.isChecked).isFalse()
+    }
+
+    @Test
+    fun screenCapturePermissionDialog_isShown_correctly() {
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
+            .thenReturn(false)
+        whenever(devicePolicyResolver.isScreenCaptureCompletelyDisabled(any<UserHandle>()))
+            .thenReturn(false)
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
+        whenever(sharedPreferences.getBoolean(HAS_APPROVED_SCREEN_RECORDING, false))
+            .thenReturn(false)
+
+        val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
+        screenRecordSwitch.isChecked = true
+
+        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(bgExecutor).execute(bgCaptor.capture())
+        bgCaptor.value.run()
+
+        val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(mainExecutor).execute(mainCaptor.capture())
+        mainCaptor.value.run()
+
+        verify(mediaProjectionMetricsLogger)
+            .notifyProjectionInitiated(
+                anyInt(),
+                eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
+            )
+        verify(factory).create(any<ScreenCapturePermissionDialogDelegate>())
+    }
+
+    @Test
+    fun noDialogsAreShown_forScreenRecord_whenApprovalIsAlreadyGiven() {
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
+            .thenReturn(false)
+        whenever(devicePolicyResolver.isScreenCaptureCompletelyDisabled(any<UserHandle>()))
+            .thenReturn(false)
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(false)
+
+        val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
+        screenRecordSwitch.isChecked = true
+
+        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(bgExecutor).execute(bgCaptor.capture())
+        bgCaptor.value.run()
+
+        verify(mediaProjectionMetricsLogger)
+            .notifyProjectionInitiated(
+                anyInt(),
+                eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
+            )
+        verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
index b90ccc0..9941661 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,35 +16,23 @@
 
 package com.android.systemui.scene.shared.flag
 
-import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.FakeFeatureFlagsImpl
-import com.android.systemui.Flags as AconfigFlags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.flags.Flags
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.ResourceBooleanFlag
-import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.flags.setFlagValue
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
-import com.android.systemui.res.R
-import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth
+import org.junit.Assume
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
 
 @SmallTest
-@RunWith(Parameterized::class)
-internal class SceneContainerFlagsTest(
-    private val testCase: TestCase,
-) : SysuiTestCase() {
-
-    @Rule @JvmField val setFlagsRule: SetFlagsRule = SetFlagsRule()
-
-    private lateinit var underTest: SceneContainerFlags
+@RunWith(AndroidJUnit4::class)
+internal class SceneContainerFlagsTest : SysuiTestCase() {
 
     @Before
     fun setUp() {
@@ -52,83 +40,39 @@
         //  Flags.SCENE_CONTAINER_ENABLED is no longer needed.
         val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED")
         field.isAccessible = true
-        field.set(null, true)
+        field.set(null, true) // note: this does not work with multivalent tests
+    }
 
-        val featureFlags =
-            FakeFeatureFlagsClassic().apply {
-                SceneContainerFlagsImpl.classicFlagTokens.forEach { flagToken ->
-                    when (flagToken) {
-                        is ResourceBooleanFlag -> set(flagToken, testCase.areAllFlagsSet)
-                        is ReleasedFlag -> set(flagToken, testCase.areAllFlagsSet)
-                        is UnreleasedFlag -> set(flagToken, testCase.areAllFlagsSet)
-                        else -> error("Unsupported flag type ${flagToken.javaClass}")
-                    }
-                }
-            }
-        // TODO(b/306421592): get the aconfig FeatureFlags from the SetFlagsRule.
-        val aconfigFlags = FakeFeatureFlagsImpl()
-
+    private fun setAconfigFlagsEnabled(enabled: Boolean) {
         listOf(
-                AconfigFlags.FLAG_SCENE_CONTAINER,
-                AconfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
+                com.android.systemui.Flags.FLAG_SCENE_CONTAINER,
+                com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
                 KeyguardShadeMigrationNssl.FLAG_NAME,
                 MediaInSceneContainerFlag.FLAG_NAME,
             )
-            .forEach { flagToken ->
-                setFlagsRule.enableFlags(flagToken)
-                aconfigFlags.setFlag(flagToken, testCase.areAllFlagsSet)
-                overrideResource(
-                    R.bool.config_sceneContainerFrameworkEnabled,
-                    testCase.isResourceConfigEnabled
-                )
-            }
-
-        underTest =
-            SceneContainerFlagsImpl(
-                context = context,
-                featureFlagsClassic = featureFlags,
-                isComposeAvailable = testCase.isComposeAvailable,
-            )
+            .forEach { flagName -> mSetFlagsRule.setFlagValue(flagName, enabled) }
     }
 
     @Test
-    fun isEnabled() {
-        assertThat(underTest.isEnabled()).isEqualTo(testCase.expectedEnabled)
+    fun isNotEnabled_withoutAconfigFlags() {
+        setAconfigFlagsEnabled(false)
+        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false)
+        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false)
     }
 
-    internal data class TestCase(
-        val isComposeAvailable: Boolean,
-        val areAllFlagsSet: Boolean,
-        val isResourceConfigEnabled: Boolean,
-        val expectedEnabled: Boolean,
-    ) {
-        override fun toString(): String {
-            return "(compose=$isComposeAvailable + flags=$areAllFlagsSet) + XML" +
-                " config=$isResourceConfigEnabled -> expected=$expectedEnabled"
-        }
+    @Test
+    fun isEnabled_withAconfigFlags_withCompose() {
+        Assume.assumeTrue(ComposeFacade.isComposeAvailable())
+        setAconfigFlagsEnabled(true)
+        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(true)
+        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(true)
     }
 
-    companion object {
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun testCases() = buildList {
-            repeat(8) { combination ->
-                val isComposeAvailable = combination and 0b100 != 0
-                val areAllFlagsSet = combination and 0b010 != 0
-                val isResourceConfigEnabled = combination and 0b001 != 0
-
-                val expectedEnabled =
-                    isComposeAvailable && areAllFlagsSet && isResourceConfigEnabled
-
-                add(
-                    TestCase(
-                        isComposeAvailable = isComposeAvailable,
-                        areAllFlagsSet = areAllFlagsSet,
-                        expectedEnabled = expectedEnabled,
-                        isResourceConfigEnabled = isResourceConfigEnabled,
-                    )
-                )
-            }
-        }
+    @Test
+    fun isNotEnabled_withAconfigFlags_withoutCompose() {
+        Assume.assumeFalse(ComposeFacade.isComposeAvailable())
+        setAconfigFlagsEnabled(true)
+        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false)
+        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
index d4e8d37..72fc65b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -10,8 +10,6 @@
 import androidx.constraintlayout.widget.Guideline
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
@@ -39,7 +37,6 @@
     lateinit var detectionNoticeView: ViewGroup
     lateinit var container: FrameLayout
 
-    var featureFlags = FakeFeatureFlags()
     lateinit var screenshotView: ViewGroup
 
     val userHandle = UserHandle.of(5)
@@ -55,7 +52,6 @@
             MessageContainerController(
                 workProfileMessageController,
                 screenshotDetectionController,
-                featureFlags
             )
         screenshotView = ConstraintLayout(mContext)
         workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
@@ -105,8 +101,6 @@
 
     @Test
     fun testOnScreenshotTakenScreenshotData_nothingToShow() {
-        featureFlags.set(Flags.SCREENSHOT_DETECTION, true)
-
         messageContainer.onScreenshotTaken(screenshotData)
 
         verify(workProfileMessageController, never()).populateView(any(), any(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index c32d259..032ec74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -177,7 +177,7 @@
         verify(context)
                 .registerReceiverForAllUsers(eq(tracker), capture(captor), isNull(), eq(handler))
         with(captor.value) {
-            assertThat(countActions()).isEqualTo(7)
+            assertThat(countActions()).isEqualTo(11)
             assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue()
             assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue()
             assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
@@ -185,6 +185,10 @@
             assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue()
             assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue()
             assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue()
+            assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue()
+            assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue()
+            assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue()
+            assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue()
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index 88c728f..b94e483 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -36,8 +36,12 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
-import dagger.Lazy
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.flow.timeout
+import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -59,7 +63,6 @@
     @Mock private lateinit var brightnessControllerFactory: BrightnessController.Factory
     @Mock private lateinit var brightnessController: BrightnessController
     @Mock private lateinit var accessibilityMgr: AccessibilityManagerWrapper
-    @Mock private lateinit var shadeInteractorLazy: Lazy<ShadeInteractor>
     @Mock private lateinit var shadeInteractor: ShadeInteractor
 
     private val clock = FakeSystemClock()
@@ -89,7 +92,6 @@
             .thenReturn(brightnessSliderController)
         `when`(brightnessSliderController.rootView).thenReturn(View(context))
         `when`(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
-        whenever(shadeInteractorLazy.get()).thenReturn(shadeInteractor)
         whenever(shadeInteractor.isQsExpanded).thenReturn(MutableStateFlow(false))
     }
 
@@ -180,6 +182,22 @@
         assertThat(activityRule.activity.isFinishing()).isFalse()
     }
 
+    @OptIn(FlowPreview::class)
+    @Test
+    fun testFinishOnQSExpanded() = runTest {
+        val isQSExpanded = MutableStateFlow(false)
+        `when`(shadeInteractor.isQsExpanded).thenReturn(isQSExpanded)
+        activityRule.launchActivity(Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG))
+
+        assertThat(activityRule.activity.isFinishing()).isFalse()
+
+        isQSExpanded.value = true
+        // Observe the activity's state until is it finishing or the timeout is reached, whatever
+        // comes first. This fixes the flakiness seen when using advanceUntilIdle().
+        activityRule.activity.finishing.timeout(100.milliseconds).takeWhile { !it }.collect {}
+        assertThat(activityRule.activity.isFinishing()).isTrue()
+    }
+
     class TestDialog(
         brightnessSliderControllerFactory: BrightnessSliderController.Factory,
         brightnessControllerFactory: BrightnessController.Factory,
@@ -194,14 +212,14 @@
             accessibilityMgr,
             shadeInteractor
         ) {
-        private var finishing = false
+        var finishing = MutableStateFlow(false)
 
         override fun isFinishing(): Boolean {
-            return finishing
+            return finishing.value
         }
 
         override fun requestFinish() {
-            finishing = true
+            finishing.value = true
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
new file mode 100644
index 0000000..5569ca9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.ViewUtils
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class GlanceableHubContainerControllerTest : SysuiTestCase() {
+    @Mock private lateinit var communalViewModel: CommunalViewModel
+    @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    @Mock private lateinit var shadeInteractor: ShadeInteractor
+
+    private lateinit var containerView: View
+    private lateinit var testableLooper: TestableLooper
+
+    private lateinit var communalInteractor: CommunalInteractor
+    private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var underTest: GlanceableHubContainerController
+
+    private val bouncerShowingFlow = MutableStateFlow(false)
+    private val shadeShowingFlow = MutableStateFlow(false)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        val withDeps = CommunalInteractorFactory.create()
+        communalInteractor = withDeps.communalInteractor
+        communalRepository = withDeps.communalRepository
+
+        underTest =
+            GlanceableHubContainerController(
+                communalInteractor,
+                communalViewModel,
+                keyguardTransitionInteractor,
+                shadeInteractor
+            )
+        testableLooper = TestableLooper.get(this)
+
+        communalRepository.setIsCommunalEnabled(true)
+
+        whenever(keyguardTransitionInteractor.isFinishedInStateWhere(any()))
+            .thenReturn(bouncerShowingFlow)
+        whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow)
+
+        overrideResource(R.dimen.communal_grid_gutter_size, SWIPE_REGION_WIDTH)
+    }
+
+    @Test
+    fun isEnabled_interactorEnabled_returnsTrue() {
+        communalRepository.setIsCommunalEnabled(true)
+
+        assertThat(underTest.isEnabled()).isTrue()
+    }
+
+    @Test
+    fun isEnabled_interactorDisabled_returnsFalse() {
+        communalRepository.setIsCommunalEnabled(false)
+
+        assertThat(underTest.isEnabled()).isFalse()
+    }
+
+    @Test
+    fun initView_notEnabled_throwsException() {
+        communalRepository.setIsCommunalEnabled(false)
+
+        assertThrows(RuntimeException::class.java) { underTest.initView(context) }
+    }
+
+    @Test
+    fun initView_calledTwice_throwsException() {
+        // First call succeeds.
+        underTest.initView(context)
+
+        // Second call throws.
+        assertThrows(RuntimeException::class.java) { underTest.initView(context) }
+    }
+
+    @Test
+    fun onTouchEvent_touchInsideGestureRegion_returnsTrue() {
+        // Communal is open.
+        communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+        initAndAttachContainerView()
+
+        // Touch events are intercepted.
+        assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue()
+    }
+
+    @Test
+    fun onTouchEvent_subsequentTouchesAfterGestureStart_returnsTrue() {
+        // Communal is open.
+        communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+        initAndAttachContainerView()
+
+        // Initial touch down is intercepted, and so are touches outside of the region, until an up
+        // event is received.
+        assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue()
+        assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
+        assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
+        assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
+    }
+
+    @Test
+    fun onTouchEvent_communalOpen_returnsTrue() {
+        // Communal is open.
+        communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+        initAndAttachContainerView()
+        testableLooper.processAllMessages()
+
+        // Touch events are intercepted.
+        assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+    }
+
+    @Test
+    fun onTouchEvent_communalAndBouncerShowing_returnsFalse() {
+        // Communal is open.
+        communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+        initAndAttachContainerView()
+
+        // Bouncer is visible.
+        bouncerShowingFlow.value = true
+        testableLooper.processAllMessages()
+
+        // Touch events are not intercepted.
+        assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+    }
+
+    @Test
+    fun onTouchEvent_communalAndShadeShowing_returnsFalse() {
+        // Communal is open.
+        communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+        initAndAttachContainerView()
+
+        shadeShowingFlow.value = true
+        testableLooper.processAllMessages()
+
+        // Touch events are not intercepted.
+        assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+    }
+
+    private fun initAndAttachContainerView() {
+        containerView = View(context)
+        // Make view clickable so that dispatchTouchEvent returns true.
+        containerView.isClickable = true
+
+        underTest.initView(containerView)
+        // Attach the view so that flows start collecting.
+        ViewUtils.attachView(containerView)
+        // Give the view a size so that determining if a touch starts at the right edge works.
+        containerView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT)
+    }
+
+    companion object {
+        private const val CONTAINER_WIDTH = 100
+        private const val CONTAINER_HEIGHT = 100
+        private const val SWIPE_REGION_WIDTH = 20
+
+        private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+        private val DOWN_IN_SWIPE_REGION_EVENT =
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
+        private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
+        private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
+
+        @BeforeClass
+        @JvmStatic
+        fun beforeClass() {
+            // Glanceable hub requires Compose, no point running any of these tests if compose isn't
+            // enabled.
+            assumeTrue(ComposeFacade.isComposeAvailable())
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 6ff7966..86d8d54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade
 
+import android.content.Context
 import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
@@ -32,7 +33,6 @@
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
 import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
@@ -45,8 +45,6 @@
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.compose.ComposeFacade.isComposeAvailable
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
@@ -69,9 +67,12 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.log.BouncerLogger
-import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -80,6 +81,7 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.gesture.TapGestureDetector
 import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
 import com.android.systemui.statusbar.notification.stack.AmbientState
@@ -94,10 +96,12 @@
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
@@ -112,9 +116,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.Optional
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -127,8 +130,6 @@
     @Mock private lateinit var centralSurfaces: CentralSurfaces
     @Mock private lateinit var dozeServiceHost: DozeServiceHost
     @Mock private lateinit var dozeScrimController: DozeScrimController
-    @Mock private lateinit var backActionInteractor: BackActionInteractor
-    @Mock private lateinit var powerInteractor: PowerInteractor
     @Mock private lateinit var dockManager: DockManager
     @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController
     @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController
@@ -150,8 +151,7 @@
     @Mock
     private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var notificationInsetsController: NotificationInsetsController
-    @Mock private lateinit var mCommunalViewModel: CommunalViewModel
-    private lateinit var mCommunalRepository: FakeCommunalRepository
+    @Mock private lateinit var mGlanceableHubContainerController: GlanceableHubContainerController
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
     @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@@ -201,8 +201,6 @@
         featureFlagsClassic.set(LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
         mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
 
-        mCommunalRepository = FakeCommunalRepository()
-
         testScope = TestScope()
         fakeClock = FakeSystemClock()
         underTest =
@@ -236,8 +234,7 @@
                 mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
                 primaryBouncerToGoneTransitionViewModel,
-                mCommunalViewModel,
-                mCommunalRepository,
+                mGlanceableHubContainerController,
                 notificationLaunchAnimationInteractor,
                 featureFlagsClassic,
                 fakeClock,
@@ -280,6 +277,12 @@
                 primaryBouncerInteractor,
                 alternateBouncerInteractor,
                 mSelectedUserInteractor,
+                { mock (JavaAdapter::class.java )},
+                { mock(AlternateBouncerViewModel::class.java) },
+                { mock(FalsingManager::class.java) },
+                { mock(SwipeUpAnywhereGestureHandler::class.java) },
+                { mock(TapGestureDetector::class.java) },
+                { mock(AlternateBouncerUdfpsIconViewModel::class.java) },
             )
         underTest.setupExpandedStatusBar()
         underTest.setDragDownHelper(dragDownHelper)
@@ -466,6 +469,16 @@
     }
 
     @Test
+    fun handleDispatchTouchEvent_glanceableHubIntercepts_returnsTrue() {
+        whenever(mGlanceableHubContainerController.onTouchEvent(DOWN_EVENT)).thenReturn(true)
+        underTest.setStatusBarViewController(phoneStatusBarViewController)
+
+        val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)
+
+        assertThat(returnVal).isTrue()
+    }
+
+    @Test
     fun shouldInterceptTouchEvent_statusBarKeyguardViewManagerShouldIntercept() {
         // down event should be intercepted by keyguardViewManager
         whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT))
@@ -546,7 +559,11 @@
             return
         }
 
-        mCommunalRepository.setIsCommunalEnabled(true)
+        whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(true)
+
+        val mockCommunalView = mock(View::class.java)
+        whenever(mGlanceableHubContainerController.initView(any<Context>()))
+            .thenReturn(mockCommunalView)
 
         val mockCommunalPlaceholder = mock(View::class.java)
         val fakeViewIndex = 20
@@ -558,7 +575,7 @@
 
         // Communal view added as a child of the container at the proper index, the stub is removed.
         verify(view).removeView(mockCommunalPlaceholder)
-        verify(view).addView(any(), eq(fakeViewIndex))
+        verify(view).addView(eq(mockCommunalView), eq(fakeViewIndex))
     }
 
     @Test
@@ -567,7 +584,7 @@
             return
         }
 
-        mCommunalRepository.setIsCommunalEnabled(false)
+        whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(false)
 
         val mockCommunalPlaceholder = mock(View::class.java)
         val fakeViewIndex = 20
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 29b1366..d9ff892 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -41,8 +41,6 @@
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.dump.logcatLogBuffer
@@ -57,8 +55,12 @@
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.log.BouncerLogger
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -67,6 +69,7 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.gesture.TapGestureDetector
 import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
 import com.android.systemui.statusbar.notification.stack.AmbientState
@@ -81,12 +84,14 @@
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
@@ -101,6 +106,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@ExperimentalCoroutinesApi
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -142,8 +148,7 @@
     private lateinit var unfoldTransitionProgressProvider:
         Optional<UnfoldTransitionProgressProvider>
     @Mock private lateinit var notificationInsetsController: NotificationInsetsController
-    @Mock private lateinit var mCommunalViewModel: CommunalViewModel
-    private lateinit var mCommunalRepository: FakeCommunalRepository
+    @Mock private lateinit var mGlanceableHubContainerController: GlanceableHubContainerController
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@@ -180,8 +185,6 @@
         whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition)
             .thenReturn(emptyFlow())
 
-        mCommunalRepository = FakeCommunalRepository()
-
         val featureFlags = FakeFeatureFlags()
         featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
         featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
@@ -221,8 +224,7 @@
                 Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
                 primaryBouncerToGoneTransitionViewModel,
-                mCommunalViewModel,
-                mCommunalRepository,
+                mGlanceableHubContainerController,
                 NotificationLaunchAnimationInteractor(NotificationLaunchAnimationRepository()),
                 featureFlags,
                 FakeSystemClock(),
@@ -265,6 +267,12 @@
                 primaryBouncerInteractor,
                 alternateBouncerInteractor,
                 mSelectedUserInteractor,
+                { Mockito.mock(JavaAdapter::class.java) },
+                { Mockito.mock(AlternateBouncerViewModel::class.java) },
+                { Mockito.mock(FalsingManager::class.java) },
+                { Mockito.mock(SwipeUpAnywhereGestureHandler::class.java) },
+                { Mockito.mock(TapGestureDetector::class.java) },
+                { Mockito.mock(AlternateBouncerUdfpsIconViewModel::class.java) },
             )
 
         controller.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ee94cbb..ee27c5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.flags.Flags.TRANSIT_CLOCK
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockId
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockMetadata
 import com.android.systemui.plugins.clocks.ClockProviderPlugin
 import com.android.systemui.plugins.clocks.ClockSettings
@@ -128,6 +129,7 @@
         override fun createClock(settings: ClockSettings): ClockController =
             createCallbacks[settings.clockId!!]!!(settings.clockId!!)
         override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!(id)
+        override fun initialize(buffers: ClockMessageBuffers?) { }
 
         fun addClock(
             id: ClockId,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index fef262f..e0e8d1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -26,6 +26,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.customization.R
+import com.android.systemui.plugins.clocks.ClockId
 import com.android.systemui.plugins.clocks.ClockSettings
 import com.android.systemui.shared.clocks.DefaultClockController.Companion.DOZE_COLOR
 import com.android.systemui.util.mockito.any
@@ -49,6 +50,9 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
+private fun DefaultClockProvider.createClock(id: ClockId): DefaultClockController =
+    createClock(ClockSettings(id, null)) as DefaultClockController
+
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class DefaultClockProviderTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
new file mode 100644
index 0000000..50349be
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.shared.notifications.data.repository
+
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.shared.settings.data.repository.FakeSecureSettingsRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class NotificationSettingsRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: NotificationSettingsRepository
+
+    private lateinit var testScope: TestScope
+    private lateinit var secureSettingsRepository: FakeSecureSettingsRepository
+
+    @Before
+    fun setUp() {
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        secureSettingsRepository = FakeSecureSettingsRepository()
+
+        underTest =
+            NotificationSettingsRepository(
+                scope = testScope.backgroundScope,
+                backgroundDispatcher = testDispatcher,
+                secureSettingsRepository = secureSettingsRepository,
+            )
+    }
+
+    @Test
+    fun testGetIsShowNotificationsOnLockscreenEnabled() =
+        testScope.runTest {
+            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled)
+
+            secureSettingsRepository.setInt(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = 1,
+            )
+            assertThat(showNotifs).isEqualTo(true)
+
+            secureSettingsRepository.setInt(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = 0,
+            )
+            assertThat(showNotifs).isEqualTo(false)
+        }
+
+    @Test
+    fun testSetIsShowNotificationsOnLockscreenEnabled() =
+        testScope.runTest {
+            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled)
+
+            underTest.setShowNotificationsOnLockscreenEnabled(true)
+            assertThat(showNotifs).isEqualTo(true)
+
+            underTest.setShowNotificationsOnLockscreenEnabled(false)
+            assertThat(showNotifs).isEqualTo(false)
+        }
+}
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 2bee7b8..d3febf5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
@@ -41,6 +41,7 @@
 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;
 
@@ -151,6 +152,7 @@
 
     @Test
     public void testOnConnectReadStatusBarSetting() {
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME);
         NotificationListener.NotificationSettingsListener settingsListener =
                 mock(NotificationListener.NotificationSettingsListener.class);
         mListener.addNotificationSettingsListener(settingsListener);
@@ -164,6 +166,7 @@
 
     @Test
     public void testOnStatusBarIconsBehaviorChanged() {
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME);
         NotificationListener.NotificationSettingsListener settingsListener =
                 mock(NotificationListener.NotificationSettingsListener.class);
         mListener.addNotificationSettingsListener(settingsListener);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java
index a6180ec..f178046 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java
@@ -54,6 +54,7 @@
 import com.android.internal.util.ContrastColorUtil;
 import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.NotificationContentDescription;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -183,7 +184,7 @@
                 .build();
         // should be ApplicationInfo
         n.extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, new Bundle());
-        StatusBarIconView.contentDescForNotification(mContext, n);
+        NotificationContentDescription.contentDescForNotification(mContext, n);
 
         // no crash, good
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
index a56fb2c..7d8cf36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
@@ -1,11 +1,10 @@
 package com.android.systemui.statusbar.notification
 
+import android.platform.test.annotations.EnableFlags
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import org.junit.Assert.assertEquals
@@ -20,8 +19,7 @@
 @RunWith(JUnit4::class)
 class RoundableTest : SysuiTestCase() {
     private val targetView: View = mock()
-    private val featureFlags = FakeFeatureFlags()
-    private val roundable = FakeRoundable(targetView = targetView, featureFlags = featureFlags)
+    private val roundable = FakeRoundable(targetView = targetView)
 
     @Test
     fun defaultConfig_shouldNotHaveRoundedCorner() {
@@ -150,36 +148,36 @@
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_radius_maxed_to_height() {
         whenever(targetView.height).thenReturn(10)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(1f, 1f, SOURCE1)
 
         assertCornerRadiiEquals(5f, 5f)
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_topRadius_maxed_to_height() {
         whenever(targetView.height).thenReturn(5)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(1f, 0f, SOURCE1)
 
         assertCornerRadiiEquals(5f, 0f)
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_bottomRadius_maxed_to_height() {
         whenever(targetView.height).thenReturn(5)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(0f, 1f, SOURCE1)
 
         assertCornerRadiiEquals(0f, 5f)
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun getCornerRadii_radii_kept() {
         whenever(targetView.height).thenReturn(100)
-        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
         roundable.requestRoundness(1f, 1f, SOURCE1)
 
         assertCornerRadiiEquals(MAX_RADIUS, MAX_RADIUS)
@@ -193,14 +191,12 @@
     class FakeRoundable(
         targetView: View,
         radius: Float = MAX_RADIUS,
-        featureFlags: FeatureFlags
     ) : Roundable {
         override val roundableState =
             RoundableState(
                 targetView = targetView,
                 roundable = this,
                 maxRadius = radius,
-                featureFlags = featureFlags
             )
 
         override val clipHeight: Int
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
index f3094cd..170f651 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
@@ -80,7 +80,7 @@
             assertThat(isPulseExpanding).isFalse()
 
             withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) }
-                .onPulseExpansionChanged(true)
+                .onPulseExpandingChanged(true)
             runCurrent()
 
             assertThat(isPulseExpanding).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index 360a373..47feccf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor
 import com.android.systemui.statusbar.notification.shared.byIsAmbient
 import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
 import com.android.systemui.statusbar.notification.shared.byIsPulsing
@@ -264,6 +265,7 @@
     interface TestComponent : SysUITestComponent<StatusBarNotificationIconsInteractor> {
 
         val activeNotificationListRepository: ActiveNotificationListRepository
+        val headsUpIconsInteractor: HeadsUpNotificationIconInteractor
         val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository
         val notificationListenerSettingsRepository: NotificationListenerSettingsRepository
 
@@ -336,6 +338,14 @@
                 .comparingElementsUsing(byIsLastMessageFromReply)
                 .doesNotContain(true)
         }
+
+    @Test
+    fun filteredEntrySet_includesIsolatedIcon() =
+        testComponent.runTest {
+            val filteredSet by collectLastValue(underTest.statusBarNotifs)
+            headsUpIconsInteractor.setIsolatedIconNotificationKey("notif5")
+            assertThat(filteredSet).comparingElementsUsing(byKey).contains("notif5")
+        }
 }
 
 private val testIcons =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index 349a35eb..c40401f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -399,4 +399,29 @@
 
             assertThat(isolatedIcon?.value?.notifKey).isEqualTo("notif1")
         }
+
+    @Test
+    fun isolatedIcon_lastMessageIsFromReply_notNull() =
+        testComponent.runTest {
+            val icon: Icon = mock()
+            headsUpViewStateRepository.isolatedNotification.value = "notif1"
+            activeNotificationsRepository.activeNotifications.value =
+                ActiveNotificationsStore.Builder()
+                    .apply {
+                        addIndividualNotif(
+                            activeNotificationModel(
+                                key = "notif1",
+                                groupKey = "group",
+                                statusBarIcon = icon,
+                                isLastMessageFromReply = true,
+                            )
+                        )
+                    }
+                    .build()
+
+            val isolatedIcon by collectLastValue(underTest.isolatedIcon)
+            runCurrent()
+
+            assertThat(isolatedIcon?.value?.notifKey).isEqualTo("notif1")
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index cb73108..0cd834d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -57,7 +57,6 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -568,9 +567,6 @@
             NotificationEntry entry,
             @InflationFlag int extraInflationFlags)
             throws Exception {
-        // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
-        //  set, but we do not want to override an existing value that is needed by a specific test.
-        mFeatureFlags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS);
 
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 mContext.LAYOUT_INFLATER_SERVICE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index c8dbdc5..d4300f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -17,6 +17,7 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState
 import com.android.systemui.util.mockito.mock
 import junit.framework.Assert.assertEquals
@@ -28,8 +29,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 /** Tests for {@link NotificationShelf}. */
 @SmallTest
@@ -53,7 +54,6 @@
         MockitoAnnotations.initMocks(this)
         mDependency.injectTestDependency(FeatureFlags::class.java, flags)
         flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal)
-        flags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS)
         val root = FrameLayout(context)
         shelf =
             LayoutInflater.from(root.context)
@@ -72,6 +72,7 @@
 
     @Test
     fun testShadeWidth_BasedOnFractionToShade() {
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME)
         setFractionToShade(0f)
         setOnLockscreen(true)
 
@@ -87,6 +88,7 @@
 
     @Test
     fun testShelfIsLong_WhenNotOnLockscreen() {
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME)
         setFractionToShade(0f)
         setOnLockscreen(false)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index ba5ba2c..ad7dee3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -82,7 +82,6 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
-import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -116,7 +115,6 @@
     private AmbientState mAmbientState;
     private TestableResources mTestableResources;
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
-    @Mock private NotificationsController mNotificationsController;
     @Mock private SysuiStatusBarStateController mBarState;
     @Mock private GroupMembershipManager mGroupMembershipManger;
     @Mock private GroupExpansionManager mGroupExpansionManager;
@@ -193,7 +191,7 @@
         mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper,
                 mNotificationStackSizeCalculator);
         mStackScroller = spy(mStackScrollerInternal);
-        mStackScroller.setNotificationsController(mNotificationsController);
+        mStackScroller.setResetUserExpandedStatesRunnable(()->{});
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
         when(mStackScrollLayoutController.getNotificationRoundnessManager())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 08ef477..f266f03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -2,21 +2,28 @@
 
 import android.annotation.DimenRes
 import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.res.R
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.EmptyShadeView
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.RoundableState
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Expect
@@ -24,6 +31,7 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
@@ -37,22 +45,26 @@
 @SmallTest
 class StackScrollAlgorithmTest : SysuiTestCase() {
 
-    @JvmField @Rule
-    var expect: Expect = Expect.create()
+    @JvmField @Rule var expect: Expect = Expect.create()
 
     private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
 
     private val hostView = FrameLayout(context)
     private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
     private val notificationRow = mock<ExpandableNotificationRow>()
+    private val notificationEntry = mock<NotificationEntry>()
     private val dumpManager = mock<DumpManager>()
+    @OptIn(ExperimentalCoroutinesApi::class)
     private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
     private val notificationShelf = mock<NotificationShelf>()
-    private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
-        layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
-    }
-    private val footerView = FooterView(context, /*attrs=*/null)
-    private val ambientState = AmbientState(
+    private val emptyShadeView =
+        EmptyShadeView(context, /* attrs= */ null).apply {
+            layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
+        }
+    private val footerView = FooterView(context, /*attrs=*/ null)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val ambientState =
+        AmbientState(
             context,
             dumpManager,
             /* sectionProvider */ { _, _ -> false },
@@ -62,13 +74,14 @@
         )
 
     private val testableResources = mContext.getOrCreateTestableResources()
+    private val featureFlags = mock<FeatureFlagsClassic>()
     private val maxPanelHeight =
         mContext.resources.displayMetrics.heightPixels -
-                px(R.dimen.notification_panel_margin_top) -
-                px(R.dimen.notification_panel_margin_bottom)
+            px(R.dimen.notification_panel_margin_top) -
+            px(R.dimen.notification_panel_margin_bottom)
 
     private fun px(@DimenRes id: Int): Float =
-            testableResources.resources.getDimensionPixelSize(id).toFloat()
+        testableResources.resources.getDimensionPixelSize(id).toFloat()
 
     private val bigGap = px(R.dimen.notification_section_divider_height)
     private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen)
@@ -76,9 +89,12 @@
     @Before
     fun setUp() {
         Assume.assumeFalse(isTv())
-
+        mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
         whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
         whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
+        whenever(notificationRow.entry).thenReturn(notificationEntry)
+        whenever(notificationRow.roundableState)
+            .thenReturn(RoundableState(notificationRow, notificationRow, 0f))
         ambientState.isSmallScreen = true
 
         hostView.addView(notificationRow)
@@ -92,7 +108,7 @@
     fun resetViewStates_defaultHun_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
         whenever(notificationRow.isHeadsUp).thenReturn(true)
-        resetViewStates_hunYTranslationIsInset()
+        resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
     }
 
     @Test
@@ -103,18 +119,87 @@
     }
 
     @Test
+    @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_yTranslationIsInset() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
-        resetViewStates_hunYTranslationIsInset()
+        resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
     }
 
     @Test
+    @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
         resetViewStates_stackMargin_changesHunYTranslation()
     }
 
     @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() {
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() {
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        resetViewStates_stackMargin_changesHunYTranslation()
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
+        // Given: the shade is open and scrolled to the bottom to show the QuickSettings
+        val maxHunTranslation = 2000f
+        ambientState.maxHeadsUpTranslation = maxHunTranslation
+        ambientState.setLayoutMinHeight(2500) // Mock the height of shade
+        ambientState.stackY = 2500f // Scroll over the max translation
+        stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
+        whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        whenever(notificationRow.isAboveShelf).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(maxHunTranslation)
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
+        // Given: the shade is open and scrolled to the bottom to show the QuickSettings
+        val bottomOfScreen = 2600f
+        val maxHunTranslation = 2000f
+        ambientState.maxHeadsUpTranslation = maxHunTranslation
+        ambientState.setLayoutMinHeight(2500) // Mock the height of shade
+        ambientState.stackY = 2500f // Scroll over the max translation
+        stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
+        stackScrollAlgorithm.setHeadsUpAppearHeightBottom(bottomOfScreen.toInt())
+        whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        whenever(notificationRow.isAboveShelf).thenReturn(true)
+        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(
+            expected = bottomOfScreen + stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
+        )
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun resetViewStates_hunAnimatingAway_newHeadsUpAnim_hunTranslatedToTopOfScreen() {
+        val topMargin = 100f
+        ambientState.maxHeadsUpTranslation = 2000f
+        ambientState.stackTopMargin = topMargin.toInt()
+        whenever(notificationRow.intrinsicHeight).thenReturn(100)
+        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(
+            expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
+        )
+    }
+
+    @Test
     fun resetViewStates_hunAnimatingAway_bottomNotClipped() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
 
@@ -136,6 +221,7 @@
     }
 
     @Test
+    @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunsOverlappingAndBottomHunAnimatingAway_bottomHunClipped() {
         val topHun = mockExpandableNotificationRow()
         val bottomHun = mockExpandableNotificationRow()
@@ -156,7 +242,7 @@
         stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
 
         val marginBottom =
-                context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
+            context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
         val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY
         val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f
         assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY)
@@ -174,33 +260,37 @@
         assertThat(notificationRow.viewState.alpha).isEqualTo(1f)
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_expansionChanging_notificationBecomesTransparent() {
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.25f,
-                expectedAlpha = 0.0f
+            expansionFraction = 0.25f,
+            expectedAlpha = 0.0f
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_expansionChangingWhileBouncerInTransit_viewBecomesTransparent() {
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.85f,
-                expectedAlpha = 0.0f
+            expansionFraction = 0.85f,
+            expectedAlpha = 0.0f
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_expansionChanging_notificationAlphaUpdated() {
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.6f,
-                expectedAlpha = getContentAlpha(0.6f)
+            expansionFraction = 0.6f,
+            expectedAlpha = getContentAlpha(0.6f)
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun resetViewStates_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() {
         val expansionFraction = 0.6f
@@ -216,13 +306,14 @@
         )
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() {
         ambientState.isSmallScreen = false
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
-                expansionFraction = 0.95f,
-                expectedAlpha = aboutToShowBouncerProgress(0.95f),
+            expansionFraction = 0.95f,
+            expectedAlpha = aboutToShowBouncerProgress(0.95f),
         )
     }
 
@@ -235,10 +326,8 @@
 
         stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
 
-        verify(notificationShelf).updateState(
-                /* algorithmState= */any(),
-                /* ambientState= */eq(ambientState)
-        )
+        verify(notificationShelf)
+            .updateState(/* algorithmState= */ any(), /* ambientState= */ eq(ambientState))
     }
 
     @Test
@@ -397,22 +486,31 @@
 
     @Test
     fun getGapForLocation_onLockscreen_returnsSmallGap() {
-        val gap = stackScrollAlgorithm.getGapForLocation(
-                /* fractionToShade= */ 0f, /* onKeyguard= */ true)
+        val gap =
+            stackScrollAlgorithm.getGapForLocation(
+                /* fractionToShade= */ 0f,
+                /* onKeyguard= */ true
+            )
         assertThat(gap).isEqualTo(smallGap)
     }
 
     @Test
     fun getGapForLocation_goingToShade_interpolatesGap() {
-        val gap = stackScrollAlgorithm.getGapForLocation(
-                /* fractionToShade= */ 0.5f, /* onKeyguard= */ true)
+        val gap =
+            stackScrollAlgorithm.getGapForLocation(
+                /* fractionToShade= */ 0.5f,
+                /* onKeyguard= */ true
+            )
         assertThat(gap).isEqualTo(smallGap * 0.5f + bigGap * 0.5f)
     }
 
     @Test
     fun getGapForLocation_notOnLockscreen_returnsBigGap() {
-        val gap = stackScrollAlgorithm.getGapForLocation(
-                /* fractionToShade= */ 0f, /* onKeyguard= */ false)
+        val gap =
+            stackScrollAlgorithm.getGapForLocation(
+                /* fractionToShade= */ 0f,
+                /* onKeyguard= */ false
+            )
         assertThat(gap).isEqualTo(bigGap)
     }
 
@@ -469,12 +567,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = false
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 0f,
-                /* maxHunY= */ 10f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 0f,
+            /* maxHunY= */ 10f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -484,12 +584,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 0f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 0f
+        )
 
         assertFalse(expandableViewState.headsUpIsVisible)
     }
@@ -499,12 +601,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ false,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 1f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ false,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 1f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -514,12 +618,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ false,
-                /* isViewEndVisible= */ true,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 1f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ false,
+            /* isViewEndVisible= */ true,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 1f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -529,12 +635,14 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.headsUpIsVisible = true
 
-        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
-                /* isShadeExpanded= */ true,
-                /* mustStayOnScreen= */ true,
-                /* isViewEndVisible= */ false,
-                /* viewEnd= */ 10f,
-                /* maxHunY= */ 1f)
+        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
+            expandableViewState,
+            /* isShadeExpanded= */ true,
+            /* mustStayOnScreen= */ true,
+            /* isViewEndVisible= */ false,
+            /* viewEnd= */ 10f,
+            /* maxHunY= */ 1f
+        )
 
         assertTrue(expandableViewState.headsUpIsVisible)
     }
@@ -544,9 +652,12 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = 50f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 1f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 1f,
+            expandableViewState
+        )
 
         // qqs (10 + 0) < viewY (50)
         assertEquals(50f, expandableViewState.yTranslation)
@@ -557,9 +668,12 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = -10f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 1f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 1f,
+            expandableViewState
+        )
 
         // qqs (10 + 0) > viewY (-10)
         assertEquals(10f, expandableViewState.yTranslation)
@@ -571,9 +685,12 @@
         expandableViewState.height = 20
         expandableViewState.yTranslation = -100f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 10f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 10f,
+            expandableViewState
+        )
 
         // newTranslation = max(10, -100) = 10
         // distToRealY = 10 - (-100f) = 110
@@ -587,9 +704,12 @@
         expandableViewState.height = 20
         expandableViewState.yTranslation = 5f
 
-        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
-                /* stackTranslation= */ 0f,
-                /* collapsedHeight= */ 10f, expandableViewState)
+        stackScrollAlgorithm.clampHunToTop(
+            /* quickQsOffsetHeight= */ 10f,
+            /* stackTranslation= */ 0f,
+            /* collapsedHeight= */ 10f,
+            expandableViewState
+        )
 
         // newTranslation = max(10, 5) = 10
         // distToRealY = 10 - 5 = 5
@@ -599,41 +719,49 @@
 
     @Test
     fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 110f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 0f)
+                /* originalCornerRoundness= */ 0f
+            )
         assertEquals(1f, currentRoundness)
     }
 
     @Test
     fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 90f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 0f)
+                /* originalCornerRoundness= */ 0f
+            )
         assertEquals(0.5f, currentRoundness)
     }
 
     @Test
     fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 0f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 0f)
+                /* originalCornerRoundness= */ 0f
+            )
         assertEquals(0f, currentRoundness)
     }
 
     @Test
     fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() {
-        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
+        val currentRoundness =
+            stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                 /* hostViewHeight= */ 100f,
                 /* stackY= */ 0f,
                 /* viewMaxHeight= */ 20f,
-                /* originalCornerRoundness= */ 1f)
+                /* originalCornerRoundness= */ 1f
+            )
         assertEquals(1f, currentRoundness)
     }
 
@@ -642,23 +770,20 @@
         // Given: shade is opened, yTranslation of HUN is 0,
         // the height of HUN equals to the height of QQS Panel,
         // and HUN fully overlaps with QQS Panel
-        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
-                px(R.dimen.qqs_layout_padding_bottom)
-        val childHunView = createHunViewMock(
-                isShadeOpen = true,
-                fullyVisible = false,
-                headerVisibleAmount = 1f
-        )
+        ambientState.stackTranslation =
+            px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
+        val childHunView =
+            createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f)
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
         algorithmState.visibleChildren.add(childHunView)
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: full shadow would be applied
@@ -670,13 +795,10 @@
         // Given: shade is opened, yTranslation of HUN is greater than 0,
         // the height of HUN is equal to the height of QQS Panel,
         // and HUN partially overlaps with QQS Panel
-        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
-                px(R.dimen.qqs_layout_padding_bottom)
-        val childHunView = createHunViewMock(
-                isShadeOpen = true,
-                fullyVisible = false,
-                headerVisibleAmount = 1f
-        )
+        ambientState.stackTranslation =
+            px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
+        val childHunView =
+            createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f)
         // Use half of the HUN's height as overlap
         childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat()
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
@@ -684,17 +806,17 @@
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should have shadow, but not as full size
         assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
         assertThat(childHunView.viewState.zTranslation)
-                .isLessThan(px(R.dimen.heads_up_pinned_elevation))
+            .isLessThan(px(R.dimen.heads_up_pinned_elevation))
     }
 
     @Test
@@ -702,28 +824,25 @@
         // Given: shade is opened, yTranslation of HUN is equal to QQS Panel's height,
         // the height of HUN is equal to the height of QQS Panel,
         // and HUN doesn't overlap with QQS Panel
-        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
-                px(R.dimen.qqs_layout_padding_bottom)
+        ambientState.stackTranslation =
+            px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
         // Mock the height of shade
         ambientState.setLayoutMinHeight(1000)
-        val childHunView = createHunViewMock(
-                isShadeOpen = true,
-                fullyVisible = true,
-                headerVisibleAmount = 1f
-        )
+        val childHunView =
+            createHunViewMock(isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f)
         // HUN doesn't overlap with QQS Panel
-        childHunView.viewState.yTranslation = ambientState.topPadding +
-                ambientState.stackTranslation
+        childHunView.viewState.yTranslation =
+            ambientState.topPadding + ambientState.stackTranslation
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
         algorithmState.visibleChildren.add(childHunView)
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should not have shadow
@@ -737,11 +856,8 @@
         ambientState.stackTranslation = -ambientState.topPadding
         // Mock the height of shade
         ambientState.setLayoutMinHeight(1000)
-        val childHunView = createHunViewMock(
-                isShadeOpen = false,
-                fullyVisible = false,
-                headerVisibleAmount = 0f
-        )
+        val childHunView =
+            createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0f)
         childHunView.viewState.yTranslation = 0f
         // Shade is closed, thus childHunView's headerVisibleAmount is 0
         childHunView.headerVisibleAmount = 0f
@@ -750,11 +866,11 @@
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should have full shadow
@@ -768,11 +884,8 @@
         ambientState.stackTranslation = -ambientState.topPadding
         // Mock the height of shade
         ambientState.setLayoutMinHeight(1000)
-        val childHunView = createHunViewMock(
-                isShadeOpen = false,
-                fullyVisible = false,
-                headerVisibleAmount = 0.5f
-        )
+        val childHunView =
+            createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0.5f)
         childHunView.viewState.yTranslation = 0f
         // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1
         // use 0.5 as headerVisibleAmount here
@@ -782,17 +895,17 @@
 
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
-                /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
-                /* StackScrollAlgorithmState= */ algorithmState,
-                /* ambientState= */ ambientState,
-                /* shouldElevateHun= */ true
+            /* i= */ 0,
+            /* childrenOnTop= */ 0.0f,
+            /* StackScrollAlgorithmState= */ algorithmState,
+            /* ambientState= */ ambientState,
+            /* shouldElevateHun= */ true
         )
 
         // Then: HUN should have shadow, but not as full size
         assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
         assertThat(childHunView.viewState.zTranslation)
-                .isLessThan(px(R.dimen.heads_up_pinned_elevation))
+            .isLessThan(px(R.dimen.heads_up_pinned_elevation))
     }
 
     @Test
@@ -862,134 +975,174 @@
         // stackScrollAlgorithm.resetViewStates is called.
         ambientState.dozeAmount = 0.5f
         setExpansionFractionWithoutShelfDuringAodToLockScreen(
-                ambientState,
-                algorithmState,
-                fraction = 0.5f
+            ambientState,
+            algorithmState,
+            fraction = 0.5f
         )
         stackScrollAlgorithm.resetViewStates(ambientState, 0)
 
         // Then: pulsingNotificationView should show at full height
         assertEquals(
-                stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView),
-                pulsingNotificationView.viewState.height
+            stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView),
+            pulsingNotificationView.viewState.height
         )
 
         // After: reset dozeAmount and expansionFraction
         ambientState.dozeAmount = 0f
         setExpansionFractionWithoutShelfDuringAodToLockScreen(
-                ambientState,
-                algorithmState,
-                fraction = 1f
+            ambientState,
+            algorithmState,
+            fraction = 1f
         )
     }
 
     // region shouldPinHunToBottomOfExpandedQs
     @Test
     fun shouldHunBeVisibleWhenScrolled_mustStayOnScreenFalse_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */false,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/false,
-            /*headsUpOnKeyguard=*/false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ false,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ false,
+                    /*headsUpOnKeyguard=*/ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldPinHunToBottomOfExpandedQs_headsUpIsVisible_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */true,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/false,
-            /*headsUpOnKeyguard=*/false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ true,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ false,
+                    /*headsUpOnKeyguard=*/ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_showingPulsing_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */true,
-            /* isOnKeyguard=*/false,
-            /* headsUpOnKeyguard= */false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ true,
+                    /* isOnKeyguard=*/ false,
+                    /* headsUpOnKeyguard= */ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_isOnKeyguard_false() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/true,
-            /* headsUpOnKeyguard= */false
-        )).isFalse()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ true,
+                    /* headsUpOnKeyguard= */ false
+                )
+            )
+            .isFalse()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_isNotOnKeyguard_true() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/false,
-            /* headsUpOnKeyguard= */false
-        )).isTrue()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ false,
+                    /* headsUpOnKeyguard= */ false
+                )
+            )
+            .isTrue()
     }
 
     @Test
     fun shouldHunBeVisibleWhenScrolled_headsUpOnKeyguard_true() {
-        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
-            /* mustStayOnScreen= */true,
-            /* headsUpIsVisible= */false,
-            /* showingPulsing= */false,
-            /* isOnKeyguard=*/true,
-            /* headsUpOnKeyguard= */true
-        )).isTrue()
+        assertThat(
+                stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+                    /* mustStayOnScreen= */ true,
+                    /* headsUpIsVisible= */ false,
+                    /* showingPulsing= */ false,
+                    /* isOnKeyguard=*/ true,
+                    /* headsUpOnKeyguard= */ true
+                )
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun shouldHunAppearFromBottom_hunAtMaxHunTranslation() {
+        ambientState.maxHeadsUpTranslation = 400f
+        val viewState =
+            ExpandableViewState().apply {
+                height = 100
+                yTranslation = ambientState.maxHeadsUpTranslation - height // move it to the max
+            }
+
+        assertTrue(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
+    }
+
+    @Test
+    fun shouldHunAppearFromBottom_hunBelowMaxHunTranslation() {
+        ambientState.maxHeadsUpTranslation = 400f
+        val viewState =
+            ExpandableViewState().apply {
+                height = 100
+                yTranslation =
+                    ambientState.maxHeadsUpTranslation - height - 1 // move it below the max
+            }
+
+        assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
     }
     // endregion
 
     private fun createHunViewMock(
-            isShadeOpen: Boolean,
-            fullyVisible: Boolean,
-            headerVisibleAmount: Float
+        isShadeOpen: Boolean,
+        fullyVisible: Boolean,
+        headerVisibleAmount: Float
     ) =
-            mock<ExpandableNotificationRow>().apply {
-                val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
-                whenever(this.viewState).thenReturn(childViewStateMock)
+        mock<ExpandableNotificationRow>().apply {
+            val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
+            whenever(this.viewState).thenReturn(childViewStateMock)
 
-                whenever(this.mustStayOnScreen()).thenReturn(true)
-                whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount)
-            }
-
+            whenever(this.mustStayOnScreen()).thenReturn(true)
+            whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount)
+        }
 
     private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) =
-            ExpandableViewState().apply {
-                // Mock the HUN's height with ambientState.topPadding +
-                // ambientState.stackTranslation
-                height = (ambientState.topPadding + ambientState.stackTranslation).toInt()
-                if (isShadeOpen && fullyVisible) {
-                    yTranslation =
-                            ambientState.topPadding + ambientState.stackTranslation
-                } else {
-                    yTranslation = 0f
-                }
-                headsUpIsVisible = fullyVisible
+        ExpandableViewState().apply {
+            // Mock the HUN's height with ambientState.topPadding +
+            // ambientState.stackTranslation
+            height = (ambientState.topPadding + ambientState.stackTranslation).toInt()
+            if (isShadeOpen && fullyVisible) {
+                yTranslation = ambientState.topPadding + ambientState.stackTranslation
+            } else {
+                yTranslation = 0f
             }
+            headsUpIsVisible = fullyVisible
+        }
 
-    private fun createPulsingViewMock(
-    ) =
-            mock<ExpandableNotificationRow>().apply {
-                whenever(this.viewState).thenReturn(ExpandableViewState())
-                whenever(this.showingPulsing()).thenReturn(true)
-            }
+    private fun createPulsingViewMock() =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.viewState).thenReturn(ExpandableViewState())
+            whenever(this.showingPulsing()).thenReturn(true)
+        }
 
     private fun setExpansionFractionWithoutShelfDuringAodToLockScreen(
-            ambientState: AmbientState,
-            algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState,
-            fraction: Float
+        ambientState: AmbientState,
+        algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState,
+        fraction: Float
     ) {
         // showingShelf: false
         algorithmState.firstViewInShelf = null
@@ -1002,11 +1155,10 @@
         ambientState.stackHeight = ambientState.stackEndHeight * fraction
     }
 
-    private fun resetViewStates_hunYTranslationIsInset() {
+    private fun resetViewStates_hunYTranslationIs(expected: Float) {
         stackScrollAlgorithm.resetViewStates(ambientState, 0)
 
-        assertThat(notificationRow.viewState.yTranslation)
-                .isEqualTo(stackScrollAlgorithm.mHeadsUpInset)
+        assertThat(notificationRow.viewState.yTranslation).isEqualTo(expected)
     }
 
     private fun resetViewStates_stackMargin_changesHunYTranslation() {
@@ -1025,13 +1177,13 @@
     }
 
     private fun resetViewStates_hunsOverlapping_bottomHunClipped(
-            topHun: ExpandableNotificationRow,
-            bottomHun: ExpandableNotificationRow
+        topHun: ExpandableNotificationRow,
+        bottomHun: ExpandableNotificationRow
     ) {
-        val topHunHeight = mContext.resources.getDimensionPixelSize(
-                R.dimen.notification_content_min_height)
-        val bottomHunHeight = mContext.resources.getDimensionPixelSize(
-                R.dimen.notification_max_heads_up_height)
+        val topHunHeight =
+            mContext.resources.getDimensionPixelSize(R.dimen.notification_content_min_height)
+        val bottomHunHeight =
+            mContext.resources.getDimensionPixelSize(R.dimen.notification_max_heads_up_height)
         whenever(topHun.intrinsicHeight).thenReturn(topHunHeight)
         whenever(bottomHun.intrinsicHeight).thenReturn(bottomHunHeight)
 
@@ -1054,8 +1206,8 @@
     }
 
     private fun resetViewStates_expansionChanging_notificationAlphaUpdated(
-            expansionFraction: Float,
-            expectedAlpha: Float,
+        expansionFraction: Float,
+        expectedAlpha: Float,
     ) {
         ambientState.isExpansionChanging = true
         ambientState.expansionFraction = expansionFraction
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
new file mode 100644
index 0000000..5a57035
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.description
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+
+private const val VIEW_HEIGHT = 100
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class StackStateAnimatorTest : SysuiTestCase() {
+
+    private lateinit var stackStateAnimator: StackStateAnimator
+    private val stackScroller: NotificationStackScrollLayout = mock()
+    private val view: ExpandableView = mock()
+    private val viewState: ExpandableViewState =
+        ExpandableViewState().apply { height = VIEW_HEIGHT }
+    private val runnableCaptor: ArgumentCaptor<Runnable> = argumentCaptor()
+    @Before
+    fun setUp() {
+        whenever(stackScroller.context).thenReturn(context)
+        whenever(view.viewState).thenReturn(viewState)
+        stackStateAnimator = StackStateAnimator(stackScroller)
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim() {
+        val topMargin = 50f
+        val expectedStartY = -topMargin - stackStateAnimator.mHeadsUpAppearStartAboveScreen
+        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR)
+        stackStateAnimator.setStackTopMargin(topMargin.toInt())
+
+        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)
+
+        verify(view).setActualHeight(VIEW_HEIGHT, false)
+        verify(view, description("should animate from the top")).translationY = expectedStartY
+        verify(view)
+            .performAddAnimation(
+                /* delay= */ 0L,
+                /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(),
+                /* isHeadsUpAppear= */ true,
+                /* onEndRunnable= */ null
+            )
+    }
+
+    @Test
+    @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
+    fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim() {
+        val screenHeight = 2000f
+        val expectedStartY = screenHeight + stackStateAnimator.mHeadsUpAppearStartAboveScreen
+        val event =
+            AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR).apply {
+                headsUpFromBottom = true
+            }
+        stackStateAnimator.setHeadsUpAppearHeightBottom(screenHeight.toInt())
+
+        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)
+
+        verify(view).setActualHeight(VIEW_HEIGHT, false)
+        verify(view, description("should animate from the bottom")).translationY = expectedStartY
+        verify(view)
+            .performAddAnimation(
+                /* delay= */ 0L,
+                /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(),
+                /* isHeadsUpAppear= */ true,
+                /* onEndRunnable= */ null
+            )
+    }
+
+    @Test
+    fun startAnimationForEvents_startsHeadsUpDisappearAnim() {
+        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
+        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)
+
+        verify(view)
+            .performRemoveAnimation(
+                /* duration= */ eq(ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()),
+                /* delay= */ eq(0L),
+                /* translationDirection= */ eq(0f),
+                /* isHeadsUpAnimation= */ eq(true),
+                /* onStartedRunnable= */ any(),
+                /* onFinishedRunnable= */ runnableCaptor.capture(),
+                /* animationListener= */ any()
+            )
+
+        runnableCaptor.value.run() // execute the end runnable
+
+        verify(view, description("should be called at the end of the animation"))
+            .removeFromTransientContainer()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt
index da543d4..cd6bb5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.testing.AndroidTestingRunner
-import android.util.Log
-import android.util.Log.TerribleFailureHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.assertDoesNotLogWtf
+import com.android.systemui.log.assertLogsWtf
 import kotlin.math.log2
 import kotlin.math.sqrt
 import org.junit.Assert
@@ -32,61 +32,36 @@
 class ViewStateTest : SysuiTestCase() {
     private val viewState = ViewState()
 
-    private var wtfHandler: TerribleFailureHandler? = null
-    private var wtfCount = 0
-
     @Suppress("DIVISION_BY_ZERO")
     @Test
     fun testWtfs() {
-        interceptWtfs()
-
         // Setting valid values doesn't cause any wtfs.
-        viewState.alpha = 0.1f
-        viewState.xTranslation = 0f
-        viewState.yTranslation = 10f
-        viewState.zTranslation = 20f
-        viewState.scaleX = 0.5f
-        viewState.scaleY = 0.25f
-
-        expectWtfs(0)
+        assertDoesNotLogWtf {
+            viewState.alpha = 0.1f
+            viewState.xTranslation = 0f
+            viewState.yTranslation = 10f
+            viewState.zTranslation = 20f
+            viewState.scaleX = 0.5f
+            viewState.scaleY = 0.25f
+        }
 
         // Setting NaN values leads to wtfs being logged, and the value not being changed.
-        viewState.alpha = 0.0f / 0.0f
-        expectWtfs(1)
+        assertLogsWtf { viewState.alpha = 0.0f / 0.0f }
         Assert.assertEquals(viewState.alpha, 0.1f)
 
-        viewState.xTranslation = Float.NaN
-        expectWtfs(2)
+        assertLogsWtf { viewState.xTranslation = Float.NaN }
         Assert.assertEquals(viewState.xTranslation, 0f)
 
-        viewState.yTranslation = log2(-10.0).toFloat()
-        expectWtfs(3)
+        assertLogsWtf { viewState.yTranslation = log2(-10.0).toFloat() }
         Assert.assertEquals(viewState.yTranslation, 10f)
 
-        viewState.zTranslation = sqrt(-1.0).toFloat()
-        expectWtfs(4)
+        assertLogsWtf { viewState.zTranslation = sqrt(-1.0).toFloat() }
         Assert.assertEquals(viewState.zTranslation, 20f)
 
-        viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY
-        expectWtfs(5)
+        assertLogsWtf { viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY }
         Assert.assertEquals(viewState.scaleX, 0.5f)
 
-        viewState.scaleY = Float.POSITIVE_INFINITY * 0
-        expectWtfs(6)
+        assertLogsWtf { viewState.scaleY = Float.POSITIVE_INFINITY * 0 }
         Assert.assertEquals(viewState.scaleY, 0.25f)
     }
-
-    private fun interceptWtfs() {
-        wtfCount = 0
-        wtfHandler =
-            Log.setWtfHandler { _: String?, e: Log.TerribleFailure, _: Boolean ->
-                Log.e("ViewStateTest", "Observed WTF: $e")
-                wtfCount++
-            }
-    }
-
-    private fun expectWtfs(expectedWtfCount: Int) {
-        Assert.assertNotNull(wtfHandler)
-        Assert.assertEquals(expectedWtfCount, wtfCount)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
index ac20683..4bfd7e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
@@ -23,8 +23,8 @@
 import android.view.Surface.ROTATION_90
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
 import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -81,7 +81,7 @@
             testScope.backgroundScope,
             mock()
         )
-    private val configurationInteractor = ConfigurationInteractorImpl(configurationRepository)
+    private val configurationInteractor = ConfigurationInteractor(configurationRepository)
 
     private lateinit var configuration: Configuration
     private lateinit var underTest: HideNotificationsInteractor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 21774aa..c17a8ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.TestMocksModule
 import com.android.systemui.collectLastValue
-import com.android.systemui.common.domain.CommonDomainLayerModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -69,7 +68,6 @@
             [
                 SysUITestModule::class,
                 ActivatableNotificationViewModelModule::class,
-                CommonDomainLayerModule::class,
                 FooterViewModelModule::class,
                 HeadlessSystemUserModeModule::class,
                 UnfoldTransitionModule.Bindings::class,
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 62d8f7f..9f4e1dd 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
@@ -23,6 +23,7 @@
 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
@@ -73,6 +74,7 @@
 
     @Test
     fun calculateWidthFor_fiveIcons_widthForFourIcons() {
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME)
         iconContainer.setActualPaddingStart(10f)
         iconContainer.setActualPaddingEnd(10f)
         iconContainer.setIconSize(10)
@@ -151,7 +153,7 @@
         iconContainer.addView(iconFive)
         assertEquals(5, iconContainer.childCount)
 
-        val width = iconContainer.calculateWidthFor(/* numIcons= */ 5f)
+        val width = iconContainer.calculateWidthFor(/* numIcons= */ 4f)
         iconContainer.setActualLayoutWidth(width.toInt())
 
         iconContainer.calculateIconXTranslations()
@@ -212,6 +214,7 @@
 
     @Test
     fun shouldForceOverflow_appearingAboveSpeedBump_true() {
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME)
         val forceOverflow =
             iconContainer.shouldForceOverflow(
                 /* i= */ 1,
@@ -228,7 +231,7 @@
             iconContainer.shouldForceOverflow(
                 /* i= */ 10,
                 /* speedBumpIndex= */ 11,
-                /* iconAppearAmount= */ 0f,
+                /* iconAppearAmount= */ 0.1f,
                 /* maxVisibleIcons= */ 5
             )
         assertTrue(forceOverflow)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt
index 7594c90..feff046 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.statusbar.phone
 
 import android.graphics.Point
+import android.testing.TestableLooper
 import android.view.Display
 import android.view.Surface
 import android.view.View
@@ -19,6 +20,7 @@
 import org.mockito.MockitoAnnotations
 
 @SmallTest
+@TestableLooper.RunWithLooper
 class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() {
 
     @Mock
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 1cc611c..54d3607 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
@@ -36,6 +36,7 @@
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
+import android.view.LayoutInflater;
 import android.view.View;
 
 import androidx.test.filters.SmallTest;
@@ -43,7 +44,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.animation.AnimatorTestRule;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.LogBuffer;
@@ -57,9 +57,7 @@
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-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.viewbinder.NotificationIconContainerStatusBarViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
@@ -70,7 +68,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewBinder;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -95,7 +92,6 @@
 
     private NotificationIconAreaController mMockNotificationAreaController;
     private ShadeExpansionStateManager mShadeExpansionStateManager;
-    private View mNotificationAreaInner;
     private OngoingCallController mOngoingCallController;
     private SystemStatusAnimationScheduler mAnimationScheduler;
     private StatusBarLocationPublisher mLocationPublisher;
@@ -274,15 +270,15 @@
 
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
 
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
 
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
-        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.VISIBLE, getNotificationAreaView().getVisibility());
 
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
 
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
     }
 
     @Test
@@ -314,7 +310,7 @@
 
         // THEN all views are hidden
         assertEquals(View.INVISIBLE, getClockView().getVisibility());
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -330,7 +326,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.VISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -347,7 +343,7 @@
 
         // THEN all views are hidden
         assertEquals(View.INVISIBLE, getClockView().getVisibility());
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
 
         // WHEN the shade is updated to no longer be open
@@ -358,7 +354,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.VISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -372,7 +368,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.VISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -386,7 +382,7 @@
 
         // THEN all views are hidden
         assertEquals(View.GONE, getClockView().getVisibility());
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -400,7 +396,7 @@
 
         // THEN all views are hidden
         assertEquals(View.GONE, getClockView().getVisibility());
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
 
         // WHEN the transition has finished
@@ -409,7 +405,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.VISIBLE, getNotificationAreaView().getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -442,7 +438,7 @@
 
         assertEquals(View.VISIBLE,
                 mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility());
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
     }
 
     @Test
@@ -507,8 +503,8 @@
         fragment.disable(DEFAULT_DISPLAY, 0, 0, true);
 
         // Notification area is hidden without delay
-        assertEquals(0f, mNotificationAreaInner.getAlpha(), 0.01);
-        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+        assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01);
+        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
     }
 
     @Test
@@ -702,7 +698,7 @@
                 mKeyguardStateController,
                 mShadeViewController,
                 mStatusBarStateController,
-                mock(StatusBarIconViewBindingFailureTracker.class),
+                mock(NotificationIconContainerStatusBarViewBinder.class),
                 mCommandQueue,
                 mCarrierConfigTracker,
                 new CollapsedStatusBarFragmentLogger(
@@ -715,10 +711,6 @@
                 mDumpManager,
                 mStatusBarWindowStateController,
                 mKeyguardUpdateMonitor,
-                mock(NotificationIconContainerStatusBarViewModel.class),
-                mock(ConfigurationState.class),
-                mock(SystemBarUtilsState.class),
-                mock(StatusBarNotificationIconViewStore.class),
                 mock(DemoModeController.class));
     }
 
@@ -731,11 +723,10 @@
 
     private void setUpNotificationIconAreaController() {
         mMockNotificationAreaController = mock(NotificationIconAreaController.class);
-
-        mNotificationAreaInner = new View(mContext);
-
-        when(mMockNotificationAreaController.getNotificationInnerAreaView()).thenReturn(
-                mNotificationAreaInner);
+        View notificationAreaInner =
+                LayoutInflater.from(mContext).inflate(R.layout.notification_icon_area, null);
+        when(mMockNotificationAreaController.getNotificationInnerAreaView())
+                .thenReturn(notificationAreaInner);
     }
 
     /**
@@ -790,4 +781,8 @@
     private View getEndSideContentView() {
         return mFragment.getView().findViewById(R.id.status_bar_end_side_content);
     }
+
+    private View getNotificationAreaView() {
+        return mFragment.getView().findViewById(R.id.notificationIcons);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index b0305a1..44fa132 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
@@ -24,6 +26,7 @@
 import com.android.settingslib.mobile.TelephonyIcons.G
 import com.android.settingslib.mobile.TelephonyIcons.THREE_G
 import com.android.settingslib.mobile.TelephonyIcons.UNKNOWN
+import com.android.systemui.Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
@@ -557,7 +560,8 @@
         }
 
     @Test
-    fun dataActivity_configOn_testIndicators() =
+    @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+    fun dataActivity_configOn_testIndicators_staticFlagOff() =
         testScope.runTest {
             // Create a new view model here so the constants are properly read
             whenever(constants.shouldShowActivityConfig).thenReturn(true)
@@ -611,6 +615,61 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+    fun dataActivity_configOn_testIndicators_staticFlagOn() =
+        testScope.runTest {
+            // Create a new view model here so the constants are properly read
+            whenever(constants.shouldShowActivityConfig).thenReturn(true)
+            createAndSetViewModel()
+
+            var inVisible: Boolean? = null
+            val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+            var outVisible: Boolean? = null
+            val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+            var containerVisible: Boolean? = null
+            val containerJob =
+                underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+            repository.dataActivityDirection.value =
+                DataActivityModel(
+                    hasActivityIn = true,
+                    hasActivityOut = false,
+                )
+
+            yield()
+
+            assertThat(inVisible).isTrue()
+            assertThat(outVisible).isFalse()
+            assertThat(containerVisible).isTrue()
+
+            repository.dataActivityDirection.value =
+                DataActivityModel(
+                    hasActivityIn = false,
+                    hasActivityOut = true,
+                )
+
+            assertThat(inVisible).isFalse()
+            assertThat(outVisible).isTrue()
+            assertThat(containerVisible).isTrue()
+
+            repository.dataActivityDirection.value =
+                DataActivityModel(
+                    hasActivityIn = false,
+                    hasActivityOut = false,
+                )
+
+            assertThat(inVisible).isFalse()
+            assertThat(outVisible).isFalse()
+            assertThat(containerVisible).isTrue()
+
+            inJob.cancel()
+            outJob.cancel()
+            containerJob.cancel()
+        }
+
+    @Test
     fun netTypeBackground_flagOff_isNull() =
         testScope.runTest {
             flags.set(NEW_NETWORK_SLICE_UI, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
index e461e3f..bbc96f70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
@@ -26,8 +26,11 @@
 
     private val recordings: MutableList<UnfoldTransitionRecording> = arrayListOf()
     private var currentRecording: UnfoldTransitionRecording? = null
+    var lastCallbackThread: Thread? = null
+        private set
 
     override fun onTransitionStarted() {
+        lastCallbackThread = Thread.currentThread()
         assertWithMessage("Trying to start a transition when it is already in progress")
             .that(currentRecording)
             .isNull()
@@ -36,6 +39,7 @@
     }
 
     override fun onTransitionProgress(progress: Float) {
+        lastCallbackThread = Thread.currentThread()
         assertWithMessage("Received transition progress event when it's not started")
             .that(currentRecording)
             .isNotNull()
@@ -43,6 +47,7 @@
     }
 
     override fun onTransitionFinishing() {
+        lastCallbackThread = Thread.currentThread()
         assertWithMessage("Received transition finishing event when it's not started")
             .that(currentRecording)
             .isNotNull()
@@ -50,6 +55,7 @@
     }
 
     override fun onTransitionFinished() {
+        lastCallbackThread = Thread.currentThread()
         assertWithMessage("Received transition finish event when it's not started")
             .that(currentRecording)
             .isNotNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
index a25469b..d864d53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.unfold.util
 
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import android.view.Surface
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -37,6 +38,7 @@
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
+@TestableLooper.RunWithLooper
 class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
 
     @Mock lateinit var rotationChangeProvider: RotationChangeProvider
@@ -48,10 +50,12 @@
     @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener>
 
     lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
+    private lateinit var testableLooper : TestableLooper
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
 
         progressProvider =
             NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, sourceProvider)
@@ -123,5 +127,6 @@
 
     private fun onRotationChanged(rotation: Int) {
         rotationListenerCaptor.value.onRotationChanged(rotation)
+        testableLooper.processAllMessages()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt
index e1e54a9..2f29b3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt
@@ -19,6 +19,7 @@
 import android.database.ContentObserver
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.unfold.TestUnfoldTransitionProvider
@@ -36,6 +37,7 @@
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
+@TestableLooper.RunWithLooper
 class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
 
     @Mock lateinit var sinkProvider: TransitionProgressListener
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt
new file mode 100644
index 0000000..5b4f4d3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold.util
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Process
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.progress.TestUnfoldProgressListener
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withTimeout
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class ScopedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
+
+    private val rootProvider = TestUnfoldTransitionProvider()
+    private val listener = TestUnfoldProgressListener()
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+    private val bgThread =
+        HandlerThread("UnfoldBgTest", Process.THREAD_PRIORITY_FOREGROUND).apply { start() }
+    private val bgHandler = Handler(bgThread.looper)
+    private val scopedProvider =
+        ScopedUnfoldTransitionProgressProvider(rootProvider).apply { addCallback(listener) }
+
+    @Test
+    fun setReadyToHandleTransition_whileTransitionRunning_propagatesCallbacks() =
+        testScope.runTest {
+            runBlockingInBg { rootProvider.onTransitionStarted() }
+
+            scopedProvider.setReadyToHandleTransition(true)
+
+            runBlockingInBg { /* sync barrier */}
+
+            listener.assertStarted()
+
+            runBlockingInBg { rootProvider.onTransitionProgress(1f) }
+
+            listener.assertLastProgress(1f)
+
+            runBlockingInBg { rootProvider.onTransitionFinished() }
+
+            listener.assertNotStarted()
+        }
+
+    @Test
+    fun setReadyToHandleTransition_whileTransitionNotRunning_callbacksInProgressThread() {
+        testScope.runTest {
+            scopedProvider.setReadyToHandleTransition(true)
+
+            val bgThread = runBlockingInBg { Thread.currentThread() }
+
+            runBlockingInBg { rootProvider.onTransitionStarted() }
+
+            listener.assertStarted()
+
+            assertThat(listener.lastCallbackThread).isEqualTo(bgThread)
+        }
+    }
+
+    @Test
+    fun setReadyToHandleTransition_beforeAnyCallback_doesNotCrash() {
+        testScope.runTest { scopedProvider.setReadyToHandleTransition(true) }
+    }
+
+    @Test
+    fun onTransitionStarted_whileNotReadyToHandleTransition_doesNotPropagate() {
+        testScope.runTest {
+            scopedProvider.setReadyToHandleTransition(false)
+
+            rootProvider.onTransitionStarted()
+
+            listener.assertNotStarted()
+        }
+    }
+
+    @Test
+    fun onTransitionStarted_defaultReadiness_doesNotPropagate() {
+        testScope.runTest {
+            rootProvider.onTransitionStarted()
+
+            listener.assertNotStarted()
+        }
+    }
+
+    @Test
+    fun onTransitionStarted_fromDifferentThreads_throws() {
+        testScope.runTest {
+            runBlockingInBg {
+                rootProvider.onTransitionStarted()
+                rootProvider.onTransitionFinished()
+            }
+            assertThrows(IllegalStateException::class.java) { rootProvider.onTransitionStarted() }
+        }
+    }
+
+    @Test
+    fun onTransitionProgress_fromDifferentThreads_throws() {
+        testScope.runTest {
+            runBlockingInBg { rootProvider.onTransitionStarted() }
+            assertThrows(IllegalStateException::class.java) {
+                rootProvider.onTransitionProgress(1f)
+            }
+        }
+    }
+
+    @Test
+    fun onTransitionFinished_fromDifferentThreads_throws() {
+        testScope.runTest {
+            runBlockingInBg { rootProvider.onTransitionStarted() }
+            assertThrows(IllegalStateException::class.java) { rootProvider.onTransitionFinished() }
+        }
+    }
+
+    @Test
+    fun onTransitionFinishing_fromDifferentThreads_throws() {
+        testScope.runTest {
+            runBlockingInBg { rootProvider.onTransitionStarted() }
+            assertThrows(IllegalStateException::class.java) { rootProvider.onTransitionFinishing() }
+        }
+    }
+
+    private fun <T> runBlockingInBg(f: () -> T): T {
+        return runBlocking {
+            withTimeout(5.seconds) {
+                suspendCancellableCoroutine { c: CancellableContinuation<T> ->
+                    bgHandler.post { c.resumeWith(Result.success(f())) }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt
index 4a38fc0..f484ea0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.unfold.util
 
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.unfold.TestUnfoldTransitionProvider
@@ -27,6 +28,7 @@
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
+@TestableLooper.RunWithLooper
 class UnfoldOnlyProgressProviderTest : SysuiTestCase() {
 
     private val listener = TestUnfoldProgressListener()
@@ -54,9 +56,7 @@
         sourceProvider.onTransitionProgress(0.5f)
         sourceProvider.onTransitionFinished()
 
-        with(listener.ensureTransitionFinished()) {
-            assertLastProgress(0.5f)
-        }
+        with(listener.ensureTransitionFinished()) { assertLastProgress(0.5f) }
     }
 
     @Test
@@ -121,8 +121,6 @@
         sourceProvider.onTransitionProgress(0.1f)
         sourceProvider.onTransitionFinished()
 
-        with(listener.ensureTransitionFinished()) {
-            assertLastProgress(0.1f)
-        }
+        with(listener.ensureTransitionFinished()) { assertLastProgress(0.1f) }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index bf851eb..6714c94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -1115,6 +1115,7 @@
                 broadcastDispatcher = fakeBroadcastDispatcher,
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
                 backgroundDispatcher = utils.testDispatcher,
+                mainDispatcher = utils.testDispatcher,
                 activityManager = activityManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index d1870b1..21d4549 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -258,6 +258,7 @@
                     broadcastDispatcher = fakeBroadcastDispatcher,
                     keyguardUpdateMonitor = keyguardUpdateMonitor,
                     backgroundDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     activityManager = activityManager,
                     refreshUsersScheduler = refreshUsersScheduler,
                     guestUserInteractor = guestUserInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index b7b24f6..d0804be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -170,6 +170,7 @@
                         broadcastDispatcher = fakeBroadcastDispatcher,
                         keyguardUpdateMonitor = keyguardUpdateMonitor,
                         backgroundDispatcher = testDispatcher,
+                        mainDispatcher = testDispatcher,
                         activityManager = activityManager,
                         refreshUsersScheduler = refreshUsersScheduler,
                         guestUserInteractor = guestUserInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
deleted file mode 100644
index a2b016f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.util.leak;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.concurrency.MessageRouterImpl;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class GarbageMonitorTest extends SysuiTestCase {
-
-    @Mock private LeakReporter mLeakReporter;
-    @Mock private TrackedGarbage mTrackedGarbage;
-    @Mock private DumpManager mDumpManager;
-    private GarbageMonitor mGarbageMonitor;
-    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mGarbageMonitor =
-                new GarbageMonitor(
-                        mContext,
-                        mFakeExecutor,
-                        new MessageRouterImpl(mFakeExecutor),
-                        new LeakDetector(null, mTrackedGarbage, null, mDumpManager),
-                        mLeakReporter,
-                        mDumpManager);
-    }
-
-    @Test
-    public void testALittleGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE);
-
-        mGarbageMonitor.reinspectGarbageAfterGc();
-
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-    }
-
-    @Test
-    public void testTransientGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        // Start the leak monitor. Nothing gets reported immediately.
-        mGarbageMonitor.startLeakMonitor();
-        mFakeExecutor.runAllReady();
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-
-        // Garbage gets reset to 0 before the leak reporte actually gets called.
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
-        mFakeExecutor.advanceClockToLast();
-        mFakeExecutor.runAllReady();
-
-        // Therefore nothing gets dumped.
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-    }
-
-    @Test
-    public void testLotsOfPersistentGarbage_dumps() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        mGarbageMonitor.reinspectGarbageAfterGc();
-
-        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-    }
-
-    @Test
-    public void testLotsOfPersistentGarbage_dumpsAfterAtime() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        // Start the leak monitor. Nothing gets reported immediately.
-        mGarbageMonitor.startLeakMonitor();
-        mFakeExecutor.runAllReady();
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-
-        mFakeExecutor.advanceClockToLast();
-        mFakeExecutor.runAllReady();
-
-        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
index e59e475..7801684 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
@@ -28,8 +28,8 @@
 import com.android.systemui.model.SysUiStateTest
 import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.BubbleEducationController
-import com.android.wm.shell.bubbles.PREF_MANAGED_EDUCATION
-import com.android.wm.shell.bubbles.PREF_STACK_EDUCATION
+import com.android.wm.shell.bubbles.ManageEducationView.Companion.PREF_MANAGED_EDUCATION
+import com.android.wm.shell.bubbles.StackEducationView.Companion.PREF_STACK_EDUCATION
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
 import org.junit.Assert.assertEquals
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 b217195..814ea19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -29,8 +29,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -50,6 +48,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import android.app.ActivityManager;
 import android.app.IActivityManager;
 import android.app.INotificationManager;
@@ -186,7 +186,7 @@
 import com.android.wm.shell.bubbles.BubbleViewInfoTask;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
 import com.android.wm.shell.bubbles.Bubbles;
-import com.android.wm.shell.bubbles.StackEducationViewKt;
+import com.android.wm.shell.bubbles.StackEducationView;
 import com.android.wm.shell.bubbles.properties.BubbleProperties;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -1930,7 +1930,7 @@
     @Test
     public void testShowStackEdu_isNotConversationBubble() {
         // Setup
-        setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false);
+        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false);
         BubbleEntry bubbleEntry = createBubbleEntry(false /* isConversation */);
         mBubbleController.updateBubble(bubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
@@ -1948,7 +1948,7 @@
     @Test
     public void testShowStackEdu_isConversationBubble() {
         // Setup
-        setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false);
+        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false);
         BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */);
         mBubbleController.updateBubble(bubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
@@ -1966,7 +1966,7 @@
     @Test
     public void testShowStackEdu_isSeenConversationBubble() {
         // Setup
-        setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, true);
+        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, true);
         BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */);
         mBubbleController.updateBubble(bubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt
index a464fa8..fa79580 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryKosmos.kt
@@ -16,10 +16,8 @@
 
 package com.android.systemui.accessibility.data.repository
 
-import android.view.accessibility.accessibilityManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.accessibilityRepository by Fixture {
-    AccessibilityRepository.invoke(a11yManager = accessibilityManager)
-}
+val Kosmos.fakeAccessibilityRepository by Fixture { FakeAccessibilityRepository() }
+val Kosmos.accessibilityRepository by Fixture { fakeAccessibilityRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeColorInversionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeColorInversionRepository.kt
new file mode 100644
index 0000000..9a3b9e2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeColorInversionRepository.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.accessibility.data.repository
+
+import android.os.UserHandle
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeColorInversionRepository : ColorInversionRepository {
+    private val userMap = mutableMapOf<Int, MutableStateFlow<Boolean>>()
+
+    override fun isEnabled(userHandle: UserHandle): StateFlow<Boolean> {
+        return getFlow(userHandle.identifier)
+    }
+
+    override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean {
+        getFlow(userHandle.identifier).value = isEnabled
+        return true
+    }
+
+    /** initializes the flow if already not */
+    private fun getFlow(userId: Int): MutableStateFlow<Boolean> {
+        return userMap.getOrPut(userId) { MutableStateFlow(false) }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 7c5696c..8486508 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -20,7 +20,6 @@
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.authentication.shared.model.AuthenticationResultModel
@@ -29,7 +28,6 @@
 import dagger.Module
 import dagger.Provides
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -40,15 +38,11 @@
     private val currentTime: () -> Long,
 ) : AuthenticationRepository {
 
-    override val authenticationChallengeResult = MutableSharedFlow<Boolean>()
-
     override val hintedPinLength: Int = HINTING_PIN_LENGTH
 
     private val _isPatternVisible = MutableStateFlow(true)
     override val isPatternVisible: StateFlow<Boolean> = _isPatternVisible.asStateFlow()
 
-    override val lockout: MutableStateFlow<AuthenticationLockoutModel?> = MutableStateFlow(null)
-
     override val hasLockoutOccurred = MutableStateFlow(false)
 
     private val _isAutoConfirmFeatureEnabled = MutableStateFlow(false)
@@ -68,8 +62,6 @@
     override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
         _isPinEnhancedPrivacyEnabled.asStateFlow()
 
-    private var failedAttemptCount = 0
-    private var lockoutEndTimestamp = 0L
     private var credentialOverride: List<Any>? = null
     private var securityMode: SecurityMode = DEFAULT_AUTHENTICATION_METHOD.toSecurityMode()
 
@@ -89,11 +81,27 @@
     }
 
     override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
-        failedAttemptCount = if (isSuccessful) 0 else failedAttemptCount + 1
-        authenticationChallengeResult.emit(isSuccessful)
+        if (isSuccessful) {
+            _failedAuthenticationAttempts.value = 0
+            _lockoutEndTimestamp = null
+            hasLockoutOccurred.value = false
+            lockoutStartedReportCount = 0
+        } else {
+            _failedAuthenticationAttempts.value++
+        }
     }
 
+    private var _failedAuthenticationAttempts = MutableStateFlow(0)
+    override val failedAuthenticationAttempts: StateFlow<Int> =
+        _failedAuthenticationAttempts.asStateFlow()
+
+    private var _lockoutEndTimestamp: Long? = null
+    override val lockoutEndTimestamp: Long?
+        get() = if (currentTime() < (_lockoutEndTimestamp ?: 0)) _lockoutEndTimestamp else null
+
     override suspend fun reportLockoutStarted(durationMs: Int) {
+        _lockoutEndTimestamp = (currentTime() + durationMs).takeIf { durationMs > 0 }
+        hasLockoutOccurred.value = true
         lockoutStartedReportCount++
     }
 
@@ -101,25 +109,10 @@
         return (credentialOverride ?: DEFAULT_PIN).size
     }
 
-    override suspend fun getFailedAuthenticationAttemptCount(): Int {
-        return failedAttemptCount
-    }
-
-    override suspend fun getLockoutEndTimestamp(): Long {
-        return lockoutEndTimestamp
-    }
-
     fun setAutoConfirmFeatureEnabled(isEnabled: Boolean) {
         _isAutoConfirmFeatureEnabled.value = isEnabled
     }
 
-    override suspend fun setLockoutDuration(durationMs: Int) {
-        lockoutEndTimestamp = if (durationMs > 0) currentTime() + durationMs else 0
-        if (durationMs > 0) {
-            hasLockoutOccurred.value = true
-        }
-    }
-
     override suspend fun checkCredential(
         credential: LockscreenCredential
     ): AuthenticationResultModel {
@@ -136,8 +129,8 @@
                 else -> error("Unexpected credential type ${credential.type}!")
             }
 
-        return if (isSuccessful || failedAttemptCount < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
-            hasLockoutOccurred.value = false
+        val failedAttempts = _failedAuthenticationAttempts.value
+        return if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
             AuthenticationResultModel(
                 isSuccessful = isSuccessful,
                 lockoutDurationMs = 0,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt
index 060ca4c..05cb059 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt
@@ -19,17 +19,11 @@
 import com.android.systemui.authentication.data.repository.authenticationRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.user.data.repository.userRepository
-import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.authenticationInteractor by
     Kosmos.Fixture {
         AuthenticationInteractor(
             applicationScope = applicationCoroutineScope,
             repository = authenticationRepository,
-            backgroundDispatcher = testDispatcher,
-            userRepository = userRepository,
-            clock = fakeSystemClock,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt
similarity index 73%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt
index 94d9a72..7f70785 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/AuthControllerKosmos.kt
@@ -14,12 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.common.domain.interactor
+package com.android.systemui.biometrics
 
-import com.android.systemui.common.ui.data.repository.configurationRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.configurationInteractor by Fixture {
-    ConfigurationInteractorImpl(repository = configurationRepository)
-}
+var Kosmos.authController by Fixture { mock<AuthController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt
new file mode 100644
index 0000000..cbfc768
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.biometrics.authController
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.udfpsOverlayInteractor by Fixture {
+    UdfpsOverlayInteractor(
+        context = applicationContext,
+        authController = authController,
+        selectedUserInteractor = selectedUserInteractor,
+        scope = applicationCoroutineScope,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt
index 5475659..8b1a1d9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt
@@ -16,14 +16,18 @@
 
 package com.android.systemui.biometrics.ui.viewmodel
 
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.statusbar.phone.systemUIDialogManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
+@ExperimentalCoroutinesApi
 val Kosmos.deviceEntryUdfpsTouchOverlayViewModel by Fixture {
     DeviceEntryUdfpsTouchOverlayViewModel(
         deviceEntryIconViewModel = deviceEntryIconViewModel,
+        alternateBouncerInteractor = alternateBouncerInteractor,
         systemUIDialogManager = systemUIDialogManager,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
new file mode 100644
index 0000000..86a4509
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.fakeSystemClock
+
+var Kosmos.alternateBouncerInteractor by
+    Kosmos.Fixture {
+        AlternateBouncerInteractor(
+            statusBarStateController = statusBarStateController,
+            keyguardStateController = mock<KeyguardStateControllerImpl>(),
+            bouncerRepository = keyguardBouncerRepository,
+            fingerprintPropertyRepository = fingerprintPropertyRepository,
+            biometricSettingsRepository = biometricSettingsRepository,
+            systemClock = fakeSystemClock,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            scope = testScope.backgroundScope,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
index 6b38d6e..050c2c9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
@@ -37,7 +37,8 @@
         MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val onConfigurationChange: Flow<Unit> = _onConfigurationChange.asSharedFlow()
 
-    private val _configurationChangeValues = MutableSharedFlow<Configuration>()
+    private val _configurationChangeValues =
+        MutableSharedFlow<Configuration>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val configurationValues: Flow<Configuration> =
         _configurationChangeValues.asSharedFlow()
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt
new file mode 100644
index 0000000..9175f09
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.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.deviceentry.data.ui.viewmodel
+
+import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor
+import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
+import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.ui.viewmodel.deviceEntryForegroundIconViewModel
+import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.udfpsAccessibilityOverlayViewModel by
+    Kosmos.Fixture {
+        UdfpsAccessibilityOverlayViewModel(
+            udfpsOverlayInteractor = udfpsOverlayInteractor,
+            accessibilityInteractor = accessibilityInteractor,
+            deviceEntryIconViewModel = deviceEntryIconViewModel,
+            deviceEntryFgIconViewModel = deviceEntryForegroundIconViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..d9c6e4f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelKosmos.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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.alternateBouncerToAodTransitionViewModel by Fixture {
+    AlternateBouncerToAodTransitionViewModel(
+        interactor = keyguardTransitionInteractor,
+        deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..e4821b0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.alternateBouncerToGoneTransitionViewModel by Fixture {
+    AlternateBouncerToGoneTransitionViewModel(
+        interactor = keyguardTransitionInteractor,
+        bouncerToGoneFlows = bouncerToGoneFlows,
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt
new file mode 100644
index 0000000..4bfe4f5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryFgIconViewModelKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.content.applicationContext
+import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.deviceEntryForegroundIconViewModel by Fixture {
+    DeviceEntryForegroundViewModel(
+        context = applicationContext,
+        configurationRepository = configurationRepository,
+        deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
+        transitionInteractor = keyguardTransitionInteractor,
+        deviceEntryIconViewModel = deviceEntryIconViewModel,
+        udfpsOverlayInteractor = udfpsOverlayInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 6557bcf..5ceefde 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -28,8 +28,10 @@
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
 
+val Kosmos.fakeDeviceEntryIconViewModelTransition by Fixture { FakeDeviceEntryIconTransition() }
+
 val Kosmos.deviceEntryIconViewModelTransitionsMock by Fixture {
-    mutableSetOf<DeviceEntryIconTransition>()
+    setOf<DeviceEntryIconTransition>(fakeDeviceEntryIconViewModelTransition)
 }
 
 val Kosmos.deviceEntryIconViewModel by Fixture {
@@ -41,7 +43,6 @@
         transitionInteractor = keyguardTransitionInteractor,
         keyguardInteractor = keyguardInteractor,
         viewModel = aodToLockscreenTransitionViewModel,
-        shadeDependentFlows = shadeDependentFlows,
         sceneContainerFlags = sceneContainerFlags,
         keyguardViewController = { statusBarKeyguardViewManager },
         deviceEntryInteractor = deviceEntryInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt
new file mode 100644
index 0000000..6d872a3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/FakeDeviceEntryIconTransition.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeDeviceEntryIconTransition : DeviceEntryIconTransition {
+    private val _deviceEntryParentViewAlpha: MutableStateFlow<Float> = MutableStateFlow(0f)
+    override val deviceEntryParentViewAlpha: Flow<Float> = _deviceEntryParentViewAlpha.asStateFlow()
+
+    fun setDeviceEntryParentViewAlpha(alpha: Float) {
+        _deviceEntryParentViewAlpha.value = alpha
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
index 6ccb3bc..5e67182 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
@@ -20,6 +20,29 @@
 import android.util.Log.TerribleFailureHandler
 import junit.framework.Assert
 
+/** Asserts that the given block does not make a call to Log.wtf */
+fun assertDoesNotLogWtf(
+    message: String = "Expected Log.wtf not to be called",
+    notLoggingBlock: () -> Unit,
+) {
+    var caught: TerribleFailureLog? = null
+    val newHandler = TerribleFailureHandler { tag, failure, system ->
+        caught = TerribleFailureLog(tag, failure, system)
+    }
+    val oldHandler = Log.setWtfHandler(newHandler)
+    try {
+        notLoggingBlock()
+    } finally {
+        Log.setWtfHandler(oldHandler)
+    }
+    caught?.let { throw AssertionError("$message: $it", it.failure) }
+}
+
+fun assertDoesNotLogWtf(
+    message: String = "Expected Log.wtf not to be called",
+    notLoggingRunnable: Runnable,
+) = assertDoesNotLogWtf(message = message) { notLoggingRunnable.run() }
+
 /**
  * Assert that the given block makes a call to Log.wtf
  *
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
new file mode 100644
index 0000000..d705248
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.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.impl.custom
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTilePackageUpdatesRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakePackageManagerAdapterFacade
+
+var Kosmos.tileSpec: TileSpec.CustomTileSpec by Kosmos.Fixture()
+
+val Kosmos.customTileStatePersister: FakeCustomTileStatePersister by
+    Kosmos.Fixture { FakeCustomTileStatePersister() }
+
+val Kosmos.customTileRepository: FakeCustomTileRepository by
+    Kosmos.Fixture {
+        FakeCustomTileRepository(
+            customTileStatePersister,
+            packageManagerAdapterFacade,
+            testScope.testScheduler,
+        )
+    }
+
+val Kosmos.customTileDefaultsRepository: FakeCustomTileDefaultsRepository by
+    Kosmos.Fixture { FakeCustomTileDefaultsRepository() }
+
+val Kosmos.customTilePackagesUpdatesRepository: FakeCustomTilePackageUpdatesRepository by
+    Kosmos.Fixture { FakeCustomTilePackageUpdatesRepository() }
+
+val Kosmos.packageManagerAdapterFacade: FakePackageManagerAdapterFacade by
+    Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
index ccf0391..ba803d8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
@@ -19,21 +19,21 @@
 import android.os.UserHandle
 import android.service.quicksettings.Tile
 import com.android.systemui.qs.external.FakeCustomTileStatePersister
-import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.flow.Flow
 
 class FakeCustomTileRepository(
-    tileSpec: TileSpec.CustomTileSpec,
     customTileStatePersister: FakeCustomTileStatePersister,
+    private val packageManagerAdapterFacade: FakePackageManagerAdapterFacade,
     testBackgroundContext: CoroutineContext,
 ) : CustomTileRepository {
 
     private val realDelegate: CustomTileRepository =
         CustomTileRepositoryImpl(
-            tileSpec,
+            packageManagerAdapterFacade.tileSpec,
             customTileStatePersister,
+            packageManagerAdapterFacade.packageManagerAdapter,
             testBackgroundContext,
         )
 
@@ -44,6 +44,10 @@
 
     override fun getTile(user: UserHandle): Tile? = realDelegate.getTile(user)
 
+    override suspend fun isTileActive(): Boolean = realDelegate.isTileActive()
+
+    override suspend fun isTileToggleable(): Boolean = realDelegate.isTileToggleable()
+
     override suspend fun updateWithTile(
         user: UserHandle,
         newTile: Tile,
@@ -55,4 +59,8 @@
         defaults: CustomTileDefaults,
         isPersistable: Boolean,
     ) = realDelegate.updateWithDefaults(user, defaults, isPersistable)
+
+    fun setTileActive(isActive: Boolean) = packageManagerAdapterFacade.setIsActive(isActive)
+
+    fun setTileToggleable(isActive: Boolean) = packageManagerAdapterFacade.setIsToggleable(isActive)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
new file mode 100644
index 0000000..c9a7655
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.data.repository
+
+import android.content.pm.ServiceInfo
+import android.os.Bundle
+import com.android.systemui.qs.external.PackageManagerAdapter
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+class FakePackageManagerAdapterFacade(
+    val tileSpec: TileSpec.CustomTileSpec,
+    val packageManagerAdapter: PackageManagerAdapter = mock {},
+) {
+
+    private var isToggleable: Boolean = false
+    private var isActive: Boolean = false
+
+    init {
+        whenever(packageManagerAdapter.getServiceInfo(eq(tileSpec.componentName), any()))
+            .thenAnswer {
+                ServiceInfo().apply {
+                    metaData =
+                        Bundle().apply {
+                            putBoolean(
+                                android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE,
+                                isToggleable
+                            )
+                            putBoolean(
+                                android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE,
+                                isActive
+                            )
+                        }
+                }
+            }
+    }
+
+    fun setIsActive(isActive: Boolean) {
+        this.isActive = isActive
+    }
+
+    fun setIsToggleable(isToggleable: Boolean) {
+        this.isToggleable = isToggleable
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/inversion/ColorInversionTileKosmos.kt
similarity index 68%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/inversion/ColorInversionTileKosmos.kt
index 94d9a72..fad34aa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/inversion/ColorInversionTileKosmos.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.common.domain.interactor
+package com.android.systemui.qs.tiles.impl.inversion
 
-import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.qs.qsEventLogger
 
-val Kosmos.configurationInteractor by Fixture {
-    ConfigurationInteractorImpl(repository = configurationRepository)
-}
+val Kosmos.qsColorInversionTileConfig by
+    Kosmos.Fixture { QSAccessibilityModule.provideColorInversionTileConfig(qsEventLogger) }
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 0b41926..25b97b3 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
@@ -91,7 +91,6 @@
 import com.android.systemui.telephony.data.repository.TelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
@@ -174,7 +173,7 @@
             mobileConnectionsRepository = mobileConnectionsRepository,
         )
 
-    val userRepository: UserRepository by lazy {
+    val userRepository: FakeUserRepository by lazy {
         FakeUserRepository().apply {
             val users = listOf(UserInfo(/* id=  */ 0, "name", /* flags= */ 0))
             setUserInfos(users)
@@ -236,9 +235,6 @@
         return AuthenticationInteractor(
             applicationScope = applicationScope(),
             repository = repository,
-            backgroundDispatcher = testDispatcher,
-            userRepository = userRepository,
-            clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
         )
     }
 
@@ -274,7 +270,6 @@
             repository = bouncerRepository,
             authenticationInteractor = authenticationInteractor,
             keyguardFaceAuthInteractor = keyguardFaceAuthInteractor,
-            flags = sceneContainerFlags,
             falsingInteractor = falsingInteractor(),
             powerInteractor = powerInteractor(),
             simBouncerInteractor = simBouncerInteractor,
@@ -312,6 +307,7 @@
             userSwitcherMenu = flowOf(createMenuActions()),
             actionButtonInteractor = actionButtonInteractor,
             simBouncerInteractor = simBouncerInteractor,
+            clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } },
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt
index 5c8fe0d..2d2f546 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.statusbar.data.repository.notificationListenerSettingsRepository
 import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor
 import com.android.wm.shell.bubbles.bubblesOptional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -43,6 +44,7 @@
     NotificationIconsInteractor(
         activeNotificationsInteractor = activeNotificationsInteractor,
         bubbles = bubblesOptional,
+        headsUpNotificationIconInteractor = headsUpNotificationIconInteractor,
         keyguardViewStateRepository = notificationsKeyguardViewStateRepository,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
similarity index 64%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
index f7f16a4..67fecb4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
@@ -16,9 +16,22 @@
 
 package com.android.systemui.statusbar.notification.icon.ui.viewbinder
 
+import com.android.systemui.common.ui.configurationState
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection
+import com.android.systemui.statusbar.ui.systemBarUtilsState
+
+val Kosmos.notificationIconContainerShelfViewBinder by Fixture {
+    NotificationIconContainerShelfViewBinder(
+        notificationIconContainerShelfViewModel,
+        configurationState,
+        systemBarUtilsState,
+        statusBarIconViewBindingFailureTracker,
+        shelfNotificationIconViewStore,
+    )
+}
 
 val Kosmos.shelfNotificationIconViewStore by Fixture {
     ShelfNotificationIconViewStore(notifCollection = notifCollection)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
index 988172c..b906b60 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-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
 
@@ -26,6 +25,5 @@
     NotificationShelfViewModel(
         interactor = notificationShelfInteractor,
         activatableViewModel = activatableNotificationViewModel,
-        icons = notificationIconContainerShelfViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorKosmos.kt
index baca8b2..4232b27 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
-import com.android.systemui.common.domain.interactor.configurationInteractor
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.power.domain.interactor.powerInteractor
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index ca5b401..04716b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -22,11 +22,9 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.shelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.statusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
 import com.android.systemui.statusbar.phone.notificationIconAreaController
-import com.android.systemui.statusbar.ui.systemBarUtilsState
 
 val Kosmos.notificationListViewBinder by Fixture {
     NotificationListViewBinder(
@@ -35,9 +33,7 @@
         configuration = configurationState,
         falsingManager = falsingManager,
         iconAreaController = notificationIconAreaController,
-        iconViewBindingFailureTracker = statusBarIconViewBindingFailureTracker,
         metricsLogger = metricsLogger,
-        shelfIconViewStore = shelfNotificationIconViewStore,
-        systemBarUtilsState = systemBarUtilsState,
+        nicBinder = notificationIconContainerShelfViewBinder,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 42c77aa..4e2dc7a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -47,6 +47,7 @@
             broadcastDispatcher = broadcastDispatcher,
             keyguardUpdateMonitor = keyguardUpdateMonitor,
             backgroundDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
             activityManager = activityManager,
             refreshUsersScheduler = refreshUsersScheduler,
             guestUserInteractor = guestUserInteractor,
diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp
index 81fd8ce..e52cefb 100644
--- a/packages/SystemUI/unfold/Android.bp
+++ b/packages/SystemUI/unfold/Android.bp
@@ -39,4 +39,7 @@
     sdk_version: "current",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt
index f9751d9..2bca272 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt
@@ -15,8 +15,11 @@
  */
 package com.android.systemui.unfold.util
 
+import android.os.Handler
+import android.os.Looper
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import java.util.concurrent.CopyOnWriteArrayList
 
 /**
  * Manages progress listeners that can have smaller lifespan than the unfold animation.
@@ -33,12 +36,13 @@
 constructor(source: UnfoldTransitionProgressProvider? = null) :
     UnfoldTransitionProgressProvider, TransitionProgressListener {
 
+    private var progressHandler: Handler? = null
     private var source: UnfoldTransitionProgressProvider? = null
 
-    private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+    private val listeners = CopyOnWriteArrayList<TransitionProgressListener>()
 
-    private var isReadyToHandleTransition = false
-    private var isTransitionRunning = false
+    @Volatile private var isReadyToHandleTransition = false
+    @Volatile private var isTransitionRunning = false
     private var lastTransitionProgress = PROGRESS_UNSET
 
     init {
@@ -70,15 +74,18 @@
      * Call it with readyToHandleTransition = false when listeners can't process the events.
      */
     fun setReadyToHandleTransition(isReadyToHandleTransition: Boolean) {
-        if (isTransitionRunning) {
-            if (isReadyToHandleTransition) {
-                listeners.forEach { it.onTransitionStarted() }
-                if (lastTransitionProgress != PROGRESS_UNSET) {
-                    listeners.forEach { it.onTransitionProgress(lastTransitionProgress) }
+        val progressHandler = this.progressHandler
+        if (isTransitionRunning && progressHandler != null) {
+            progressHandler.post {
+                if (isReadyToHandleTransition) {
+                    listeners.forEach { it.onTransitionStarted() }
+                    if (lastTransitionProgress != PROGRESS_UNSET) {
+                        listeners.forEach { it.onTransitionProgress(lastTransitionProgress) }
+                    }
+                } else {
+                    isTransitionRunning = false
+                    listeners.forEach { it.onTransitionFinished() }
                 }
-            } else {
-                isTransitionRunning = false
-                listeners.forEach { it.onTransitionFinished() }
             }
         }
         this.isReadyToHandleTransition = isReadyToHandleTransition
@@ -98,6 +105,7 @@
     }
 
     override fun onTransitionStarted() {
+        assertInProgressThread()
         isTransitionRunning = true
         if (isReadyToHandleTransition) {
             listeners.forEach { it.onTransitionStarted() }
@@ -105,6 +113,7 @@
     }
 
     override fun onTransitionProgress(progress: Float) {
+        assertInProgressThread()
         if (isReadyToHandleTransition) {
             listeners.forEach { it.onTransitionProgress(progress) }
         }
@@ -112,12 +121,14 @@
     }
 
     override fun onTransitionFinishing() {
+        assertInProgressThread()
         if (isReadyToHandleTransition) {
             listeners.forEach { it.onTransitionFinishing() }
         }
     }
 
     override fun onTransitionFinished() {
+        assertInProgressThread()
         if (isReadyToHandleTransition) {
             listeners.forEach { it.onTransitionFinished() }
         }
@@ -125,6 +136,21 @@
         lastTransitionProgress = PROGRESS_UNSET
     }
 
+    private fun assertInProgressThread() {
+        val cachedProgressHandler = progressHandler
+        if (cachedProgressHandler == null) {
+            val thisLooper = Looper.myLooper() ?: error("This thread is expected to have a looper.")
+            progressHandler = Handler(thisLooper)
+        } else {
+            check(cachedProgressHandler.looper.isCurrentThread) {
+                """Receiving unfold transition callback from different threads.
+                    |Current: ${Thread.currentThread()}
+                    |expected: ${cachedProgressHandler.looper.thread}"""
+                    .trimMargin()
+            }
+        }
+    }
+
     companion object {
         private const val PROGRESS_UNSET = -1f
     }
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 13908f1..491ed22 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -55,6 +55,7 @@
 android.os.Parcel
 android.os.Parcelable
 android.os.Process
+android.os.ServiceSpecificException
 android.os.SystemClock
 android.os.ThreadLocalWorkSource
 android.os.TimestampedValue
@@ -90,6 +91,7 @@
 android.database.Cursor
 android.database.CursorIndexOutOfBoundsException
 android.database.CursorJoiner
+android.database.CursorWindow
 android.database.CursorWrapper
 android.database.DataSetObservable
 android.database.DataSetObserver
@@ -97,6 +99,9 @@
 android.database.MatrixCursor$RowBuilder
 android.database.MergeCursor
 android.database.Observable
+android.database.SQLException
+android.database.sqlite.SQLiteClosable
+android.database.sqlite.SQLiteException
 
 android.text.TextUtils
 android.text.TextUtils$SimpleStringSplitter
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 77a5e3d..cab2d74 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.server.appwidget;
 
+import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath;
 import static android.content.Context.KEYGUARD_SERVICE;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@@ -72,6 +73,7 @@
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
@@ -143,6 +145,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -276,7 +279,12 @@
         mKeyguardManager = (KeyguardManager) mContext.getSystemService(KEYGUARD_SERVICE);
         mDevicePolicyManagerInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
-        mSaveStateHandler = BackgroundThread.getHandler();
+        if (removeAppWidgetServiceIoFromCriticalPath()) {
+            mSaveStateHandler = new Handler(BackgroundThread.get().getLooper(),
+                    this::handleSaveMessage);
+        } else {
+            mSaveStateHandler = BackgroundThread.getHandler();
+        }
         final ServiceThread serviceThread = new ServiceThread(TAG,
                 android.os.Process.THREAD_PRIORITY_FOREGROUND, false /* allowIo */);
         serviceThread.start();
@@ -313,6 +321,40 @@
         mMaxWidgetBitmapMemory = 6 * size.x * size.y;
     }
 
+    private boolean handleSaveMessage(Message msg) {
+        final int userId = msg.what;
+        SparseArray<byte[]> userIdToBytesMapping;
+        synchronized (mLock) {
+            // No need to enforce unlocked state when there is no caller. User can be in the
+            // stopping state or removed by the time the message is processed
+            ensureGroupStateLoadedLocked(userId, false /* enforceUserUnlockingOrUnlocked */);
+            userIdToBytesMapping = saveStateToByteArrayLocked(userId);
+        }
+
+        for (int i = 0; i < userIdToBytesMapping.size(); i++) {
+            int currentProfileId = userIdToBytesMapping.keyAt(i);
+            byte[] currentStateByteArray = userIdToBytesMapping.valueAt(i);
+            AtomicFile currentFile = getSavedStateFile(currentProfileId);
+            FileOutputStream fileStream;
+            try {
+                fileStream = currentFile.startWrite();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to start writing stream", e);
+                continue;
+            }
+
+            try {
+                fileStream.write(currentStateByteArray);
+                currentFile.finishWrite(fileStream);
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to write state byte stream to file", e);
+                currentFile.failWrite(fileStream);
+            }
+        }
+
+        return true;
+    }
+
     private void registerBroadcastReceiver() {
         // Register for broadcasts about package install, etc., so we can
         // update the provider list.
@@ -559,10 +601,11 @@
                 onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId);
             } else if (provider.maskedBySuspendedPackage) {
                 showBadge = mUserManager.hasBadge(appUserId);
-                final String suspendingPackage = mPackageManagerInternal.getSuspendingPackage(
+                final UserPackage suspendingPackage = mPackageManagerInternal.getSuspendingPackage(
                         appInfo.packageName, appUserId);
                 // TODO(b/281839596): don't rely on platform always meaning suspended by admin.
-                if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) {
+                if (suspendingPackage != null
+                        && PLATFORM_PACKAGE_NAME.equals(suspendingPackage.packageName)) {
                     onClickIntent = mDevicePolicyManagerInternal.createShowAdminSupportIntent(
                             appUserId, true);
                 } else {
@@ -1942,7 +1985,12 @@
     }
 
     private void saveGroupStateAsync(int groupId) {
-        mSaveStateHandler.post(new SaveStateRunnable(groupId));
+        if (removeAppWidgetServiceIoFromCriticalPath()) {
+            mSaveStateHandler.removeMessages(groupId);
+            mSaveStateHandler.sendEmptyMessage(groupId);
+        } else {
+            mSaveStateHandler.post(new SaveStateRunnable(groupId));
+        }
     }
 
     private void updateAppWidgetInstanceLocked(Widget widget, RemoteViews views,
@@ -3102,6 +3150,23 @@
     }
 
     @GuardedBy("mLock")
+    private @NonNull SparseArray<byte[]> saveStateToByteArrayLocked(int userId) {
+        tagProvidersAndHosts();
+
+        final int[] profileIds = mSecurityPolicy.getEnabledGroupProfileIds(userId);
+        SparseArray<byte[]> userIdToBytesMapping = new SparseArray<>();
+
+        for (int profileId : profileIds) {
+            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            if (writeProfileStateToStreamLocked(outputStream, profileId)) {
+                userIdToBytesMapping.put(profileId, outputStream.toByteArray());
+            }
+        }
+
+        return userIdToBytesMapping;
+    }
+
+    @GuardedBy("mLock")
     private void saveStateLocked(int userId) {
         tagProvidersAndHosts();
 
@@ -3115,7 +3180,7 @@
             FileOutputStream stream;
             try {
                 stream = file.startWrite();
-                if (writeProfileStateToFileLocked(stream, profileId)) {
+                if (writeProfileStateToStreamLocked(stream, profileId)) {
                     file.finishWrite(stream);
                 } else {
                     file.failWrite(stream);
@@ -3156,7 +3221,7 @@
     }
 
     @GuardedBy("mLock")
-    private boolean writeProfileStateToFileLocked(FileOutputStream stream, int userId) {
+    private boolean writeProfileStateToStreamLocked(OutputStream stream, int userId) {
         int N;
 
         try {
diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
index d9741c8..4a6d5c9b 100644
--- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
+++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
@@ -25,9 +25,6 @@
 import android.service.autofill.FillResponse;
 import android.util.Slog;
 
-import java.util.Objects;
-
-
 /**
  * Requests autofill response from a Remote Autofill Service. This autofill service can be
  * either a Credential Autofill Service or the user-opted autofill service.
@@ -51,7 +48,6 @@
 
     private final RemoteFillService mRemoteFillService;
     private final SecondaryProviderCallback mCallback;
-    private FillRequest mLastFillRequest;
     private int mLastFlag;
 
     SecondaryProviderHandler(
@@ -97,17 +93,11 @@
     }
 
     /**
-     * Requests a new fill response. If the fill request is same as the last requested fill request,
-     * then the request is duped.
+     * Requests a new fill response.
      */
     public void onFillRequest(FillRequest pendingFillRequest, int flag) {
-        if (Objects.equals(pendingFillRequest, mLastFillRequest)) {
-            Slog.v(TAG, "Deduping fill request to secondary provider.");
-            return;
-        }
         Slog.v(TAG, "Requesting fill response to secondary provider.");
         mLastFlag = flag;
-        mLastFillRequest = pendingFillRequest;
         mRemoteFillService.onFillRequest(pendingFillRequest);
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index d527ce0..c4e8f12 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -367,6 +367,9 @@
     @GuardedBy("mLock")
     private SparseArray<FillResponse> mResponses;
 
+    @GuardedBy("mLock")
+    private SparseArray<FillResponse> mSecondaryResponses;
+
     /**
      * Contexts read from the app; they will be updated (sanitized, change values for save) before
      * sent to {@link AutofillService}. Ordered by the time they were read.
@@ -713,7 +716,14 @@
                         mPendingFillRequest.getDelayedFillIntentSender());
             }
             mLastFillRequest = mPendingFillRequest;
-            mRemoteFillService.onFillRequest(mPendingFillRequest);
+            if (shouldRequestSecondaryProvider(mPendingFillRequest.getFlags())
+                    && mSecondaryProviderHandler != null) {
+                Slog.v(TAG, "Requesting fill response to secondary provider.");
+                mSecondaryProviderHandler.onFillRequest(mPendingFillRequest,
+                        mPendingFillRequest.getFlags());
+            } else if (mRemoteFillService != null) {
+                mRemoteFillService.onFillRequest(mPendingFillRequest);
+            }
             mPendingInlineSuggestionsRequest = null;
             mWaitForInlineRequest = false;
             mPendingFillRequest = null;
@@ -1196,7 +1206,8 @@
     @GuardedBy("mLock")
     private void requestNewFillResponseLocked(@NonNull ViewState viewState, int newState,
             int flags) {
-        final FillResponse existingResponse = viewState.getResponse();
+        final FillResponse existingResponse = shouldRequestSecondaryProvider(flags)
+                ? viewState.getSecondaryResponse() : viewState.getResponse();
         mFillRequestEventLogger.startLogForNewRequest();
         mRequestCount++;
         mFillRequestEventLogger.maybeSetAppPackageUid(uid);
@@ -1804,6 +1815,10 @@
             return;
         }
         synchronized (mLock) {
+            if (mSecondaryResponses == null) {
+                mSecondaryResponses = new SparseArray<>(2);
+            }
+            mSecondaryResponses.put(fillResponse.getRequestId(), fillResponse);
             setViewStatesLocked(fillResponse, ViewState.STATE_FILLABLE, /* clearResponse= */ false,
                     /* isPrimary= */ false);
 
@@ -3980,7 +3995,7 @@
         }
 
         // If it's not, then check if it should start a partition.
-        if (shouldStartNewPartitionLocked(id)) {
+        if (shouldStartNewPartitionLocked(id, flags)) {
             if (sDebug) {
                 Slog.d(TAG, "Starting partition or augmented request for view id " + id + ": "
                         + viewState.getStateAsString());
@@ -4008,9 +4023,11 @@
      * @return {@code true} if a new partition should be started
      */
     @GuardedBy("mLock")
-    private boolean shouldStartNewPartitionLocked(@NonNull AutofillId id) {
+    private boolean shouldStartNewPartitionLocked(@NonNull AutofillId id, int flags) {
         final ViewState currentView = mViewStates.get(id);
-        if (mResponses == null) {
+        SparseArray<FillResponse> responses = shouldRequestSecondaryProvider(flags)
+                ? mSecondaryResponses : mResponses;
+        if (responses == null) {
             return currentView != null && (currentView.getState()
                     & ViewState.STATE_PENDING_CREATE_INLINE_REQUEST) == 0;
         }
@@ -4022,7 +4039,7 @@
             return true;
         }
 
-        final int numResponses = mResponses.size();
+        final int numResponses = responses.size();
         if (numResponses >= AutofillManagerService.getPartitionMaxCount()) {
             Slog.e(TAG, "Not starting a new partition on " + id + " because session " + this.id
                     + " reached maximum of " + AutofillManagerService.getPartitionMaxCount());
@@ -4030,7 +4047,7 @@
         }
 
         for (int responseNum = 0; responseNum < numResponses; responseNum++) {
-            final FillResponse response = mResponses.valueAt(responseNum);
+            final FillResponse response = responses.valueAt(responseNum);
 
             if (ArrayUtils.contains(response.getIgnoredIds(), id)) {
                 return false;
@@ -4066,6 +4083,10 @@
     }
 
     boolean shouldRequestSecondaryProvider(int flags) {
+        if (!mService.isAutofillCredmanIntegrationEnabled()
+                || mSecondaryProviderHandler == null) {
+            return false;
+        }
         if (mIsPrimaryCredential) {
             return (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) == 0;
         } else {
@@ -4205,12 +4226,6 @@
                 }
                 break;
             case ACTION_VIEW_ENTERED:
-                if (shouldRequestSecondaryProvider(flags)
-                        && mSecondaryProviderHandler != null
-                        && mAssistReceiver.mLastFillRequest != null) {
-                    mSecondaryProviderHandler.onFillRequest(mAssistReceiver.mLastFillRequest,
-                            flags);
-                }
                 mLatencyBaseTime = SystemClock.elapsedRealtime();
                 boolean wasPreviouslyFillDialog = mPreviouslyFillDialogPotentiallyStarted;
                 mPreviouslyFillDialogPotentiallyStarted = false;
@@ -4225,6 +4240,19 @@
                     viewState.setCurrentValue(value);
                 }
 
+                if (shouldRequestSecondaryProvider(flags)) {
+                    if (requestNewFillResponseOnViewEnteredIfNecessaryLocked(
+                            id, viewState, flags)) {
+                        Slog.v(TAG, "Started a new fill request for secondary provider.");
+                        return;
+                    }
+                    // If the ViewState is ready to be displayed, onReady() will be called.
+                    viewState.update(value, virtualBounds, flags);
+
+                    // return here because primary provider logic is not applicable.
+                    return;
+                }
+
                 if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
                     if (sDebug) Slog.d(TAG, "Ignoring VIEW_ENTERED on URL BAR (id=" + id + ")");
                     return;
diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java
index b0bb9ec..fec5aa5 100644
--- a/services/autofill/java/com/android/server/autofill/ViewState.java
+++ b/services/autofill/java/com/android/server/autofill/ViewState.java
@@ -162,6 +162,11 @@
         return mPrimaryFillResponse;
     }
 
+    @Nullable
+    FillResponse getSecondaryResponse() {
+        return mSecondaryFillResponse;
+    }
+
     void setResponse(FillResponse response) {
         setResponse(response, /* isPrimary= */ true);
     }
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index acb5911..d08a97e 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -19,7 +19,13 @@
     defaults: ["platform_service_defaults"],
     srcs: [":services.backup-sources"],
     libs: ["services.core"],
-    static_libs: ["app-compat-annotations", "backup_flags_lib"],
+    static_libs: [
+        "app-compat-annotations",
+        "backup_flags_lib",
+    ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 aconfig_declarations {
diff --git a/services/companion/Android.bp b/services/companion/Android.bp
index 550e17b..2bfdd0a 100644
--- a/services/companion/Android.bp
+++ b/services/companion/Android.bp
@@ -31,4 +31,7 @@
         "virtualdevice_flags_lib",
         "virtual_camera_service_aidl-java",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index db40fc4..6c77018 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -19,6 +19,7 @@
 import static android.content.pm.PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
 import static android.content.pm.PackageManager.GET_CONFIGURATIONS;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.os.Binder.getCallingUid;
 
 import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
 import static com.android.server.companion.CompanionDeviceManagerService.TAG;
@@ -41,6 +42,7 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.Signature;
 import android.os.Binder;
+import android.os.Process;
 import android.util.Log;
 import android.util.Slog;
 
@@ -80,6 +82,11 @@
 
     static void enforceUsesCompanionDeviceFeature(@NonNull Context context,
             @UserIdInt int userId, @NonNull String packageName) {
+        // Allow system server to create CDM associations without FEATURE_COMPANION_DEVICE_SETUP
+        if (getCallingUid() == Process.SYSTEM_UID) {
+            return;
+        }
+
         String requiredFeature = FEATURE_COMPANION_DEVICE_SETUP;
 
         FeatureInfo[] requestedFeatures = getPackageInfo(context, userId, packageName).reqFeatures;
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index df74770..62c6703 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -130,7 +130,7 @@
         synchronized (mTransports) {
             for (int i = 0; i < associationIds.length; i++) {
                 if (mTransports.contains(associationIds[i])) {
-                    mTransports.get(associationIds[i]).requestForResponse(message, data);
+                    mTransports.get(associationIds[i]).sendMessage(message, data);
                 }
             }
         }
@@ -220,7 +220,7 @@
             if (transport == null) {
                 return CompletableFuture.failedFuture(new IOException("Missing transport"));
             }
-            return transport.requestForResponse(MESSAGE_REQUEST_PERMISSION_RESTORE, data);
+            return transport.sendMessage(MESSAGE_REQUEST_PERMISSION_RESTORE, data);
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/transport/Transport.java b/services/companion/java/com/android/server/companion/transport/Transport.java
index 32d4061..22b18ac 100644
--- a/services/companion/java/com/android/server/companion/transport/Transport.java
+++ b/services/companion/java/com/android/server/companion/transport/Transport.java
@@ -16,6 +16,9 @@
 
 package com.android.server.companion.transport;
 
+import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE;
+import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_PING;
+import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_TO_WEARABLE;
 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC;
 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PERMISSION_RESTORE;
 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PING;
@@ -80,6 +83,10 @@
         return (message & 0xFF000000) == 0x33000000;
     }
 
+    private static boolean isOneway(int message) {
+        return (message & 0xFF000000) == 0x43000000;
+    }
+
     @GuardedBy("mPendingRequests")
     protected final SparseArray<CompletableFuture<byte[]>> mPendingRequests =
             new SparseArray<>();
@@ -134,6 +141,42 @@
     protected abstract void sendMessage(int message, int sequence, @NonNull byte[] data)
             throws IOException;
 
+    /**
+     * Send a message using this transport. If the message was a request, then the returned Future
+     * object will complete successfully only if the remote device both received and processed it
+     * as expected. If the message was a send-and-forget type message, then the Future object will
+     * resolve successfully immediately (with null) upon sending the message.
+     *
+     * @param message the message type
+     * @param data the message payload
+     * @return Future object containing the result of the sent message.
+     */
+    public Future<byte[]> sendMessage(int message, byte[] data) {
+        final CompletableFuture<byte[]> pending = new CompletableFuture<>();
+        if (isOneway(message)) {
+            return sendAndForget(message, data);
+        } else if (isRequest(message)) {
+            return requestForResponse(message, data);
+        } else {
+            Slog.w(TAG, "Failed to send message 0x" + Integer.toHexString(message));
+            pending.completeExceptionally(new IllegalArgumentException(
+                    "The message being sent must be either a one-way or a request."
+            ));
+        }
+        return pending;
+    }
+
+    /**
+     * @deprecated Method was renamed to sendMessage(int, byte[]) to support both
+     * send-and-forget type messages as well as wait-for-response type messages.
+     *
+     * @param message request message type
+     * @param data the message payload
+     * @return future object containing the result of the request.
+     *
+     * @see #sendMessage(int, byte[])
+     */
+    @Deprecated
     public Future<byte[]> requestForResponse(int message, byte[] data) {
         if (DEBUG) Slog.d(TAG, "Requesting for response");
         final int sequence = mNextSequence.incrementAndGet();
@@ -154,6 +197,20 @@
         return pending;
     }
 
+    private Future<byte[]> sendAndForget(int message, byte[]data) {
+        if (DEBUG) Slog.d(TAG, "Sending a one-way message");
+        final CompletableFuture<byte[]> pending = new CompletableFuture<>();
+
+        try {
+            sendMessage(message, -1, data);
+            pending.complete(null);
+        } catch (IOException e) {
+            pending.completeExceptionally(e);
+        }
+
+        return pending;
+    }
+
     protected final void handleMessage(int message, int sequence, @NonNull byte[] data)
             throws IOException {
         if (DEBUG) {
@@ -162,7 +219,9 @@
                     + " from association " + mAssociationId);
         }
 
-        if (isRequest(message)) {
+        if (isOneway(message)) {
+            processOneway(message, data);
+        } else if (isRequest(message)) {
             try {
                 processRequest(message, sequence, data);
             } catch (IOException e) {
@@ -175,6 +234,21 @@
         }
     }
 
+    private void processOneway(int message, byte[] data) {
+        switch (message) {
+            case MESSAGE_ONEWAY_PING:
+            case MESSAGE_ONEWAY_FROM_WEARABLE:
+            case MESSAGE_ONEWAY_TO_WEARABLE: {
+                callback(message, data);
+                break;
+            }
+            default: {
+                Slog.w(TAG, "Ignoring unknown message 0x" + Integer.toHexString(message));
+                break;
+            }
+        }
+    }
+
     private void processRequest(int message, int sequence, byte[] data)
             throws IOException {
         switch (message) {
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
index a159a5e..5a548fd 100644
--- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
+++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
@@ -54,9 +54,7 @@
           "exclude-annotation": "android.support.test.filters.FlakyTest"
         }
       ]
-    }
-  ],
-  "postsubmit": [
+    },
     {
       "name": "CtsVirtualDevicesCameraTestCases",
       "options": [
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index 6940ffe..f24c4cc 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -70,7 +70,7 @@
 
             @Override
             public void onProcessCaptureRequest(int streamId, int frameId) throws RemoteException {
-                camera.onProcessCaptureRequest(streamId, frameId, /*metadata=*/ null);
+                camera.onProcessCaptureRequest(streamId, frameId);
             }
 
             @Override
diff --git a/services/core/Android.bp b/services/core/Android.bp
index f5a80d8..dd001ec 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -100,10 +100,9 @@
     name: "services.core.unboosted",
     defaults: [
         "platform_service_defaults",
-        "android.hardware.power-java_static",
+        "android.hardware.power-java_shared",
     ],
     srcs: [
-        ":android.hardware.biometrics.face-V4-java-source",
         ":android.hardware.tv.hdmi.connection-V1-java-source",
         ":android.hardware.tv.hdmi.earc-V1-java-source",
         ":statslog-art-java-gen",
@@ -153,7 +152,7 @@
     ],
 
     static_libs: [
-        "android.frameworks.location.altitude-V1-java", // AIDL
+        "android.frameworks.location.altitude-V2-java", // AIDL
         "android.frameworks.vibrator-V1-java", // AIDL
         "android.hardware.authsecret-V1.0-java",
         "android.hardware.authsecret-V1-java",
@@ -213,6 +212,9 @@
         "-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
         "-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_genrule {
@@ -231,6 +233,9 @@
 java_library {
     name: "services.core",
     static_libs: ["services.core.priorityboosted"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library_host {
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index 136692e..08093c0 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -289,11 +289,11 @@
      *
      * @param suspendedPackage The package that has been suspended.
      * @param userId The user for which to check.
-     * @return Name of the package that suspended the given package. Returns {@code null} if the
-     * given package is not currently suspended and the platform package name - i.e.
-     * {@code "android"} - if the package was suspended by a device admin.
+     * @return User id and package name of the package that suspended the given package. Returns
+     * {@code null} if the given package is not currently suspended and the platform package name
+     * - i.e. {@code "android"} - if the package was suspended by a device admin.
      */
-    public abstract String getSuspendingPackage(String suspendedPackage, int userId);
+    public abstract UserPackage getSuspendingPackage(String suspendedPackage, int userId);
 
     /**
      * Suspend or unsuspend packages upon admin request.
@@ -312,13 +312,13 @@
      * suspended application.
      *
      * @param suspendedPackage The package that has been suspended.
-     * @param suspendingPackage
+     * @param suspendingPackage The package responsible for suspension.
      * @param userId The user for which to check.
      * @return A {@link SuspendDialogInfo} object describing the dialog to be shown.
      */
     @Nullable
     public abstract SuspendDialogInfo getSuspendedDialogInfo(String suspendedPackage,
-            String suspendingPackage, int userId);
+            UserPackage suspendingPackage, int userId);
 
     /**
      * Gets any distraction flags set via
@@ -1168,14 +1168,14 @@
     public abstract void clearBlockUninstallForUser(@UserIdInt int userId);
 
     /**
-     * Unsuspends all packages suspended by the given package for the user.
+     * Unsuspends all packages suspended by an admin for the user.
      */
-    public abstract void unsuspendForSuspendingPackage(String suspendingPackage, int userId);
+    public abstract void unsuspendAdminSuspendedPackages(int userId);
 
     /**
-     * Returns {@code true} if the package is suspending any packages for the user.
+     * Returns {@code true} if an admin is suspending any packages for the user.
      */
-    public abstract boolean isSuspendingAnyPackages(String suspendingPackage, int userId);
+    public abstract boolean isAdminSuspendingAnyPackages(int userId);
 
     /**
      * Register to listen for loading progress of an installed package.
@@ -1463,4 +1463,9 @@
      */
     @NonNull
     public abstract PackageArchiver getPackageArchiver();
+
+    /**
+     * Returns true if the device is upgrading from an SDK version lower than the one specified.
+     */
+    public abstract boolean isUpgradingFromLowerThan(int sdkVersion);
 }
diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index 329aac6..9f279b1 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -48,8 +48,6 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.am.DropboxRateLimiter;
-import com.android.server.os.TombstoneProtos;
-import com.android.server.os.TombstoneProtos.Tombstone;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -62,14 +60,11 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.attribute.PosixFilePermissions;
-import java.util.AbstractMap;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.Map;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
 /**
  * Performs a number of miscellaneous, non-system-critical actions
@@ -337,12 +332,12 @@
      *
      * @param ctx Context
      * @param tombstone path to the tombstone
-     * @param tombstoneProto the parsed proto tombstone
+     * @param proto whether the tombstone is stored as proto
      * @param processName the name of the process corresponding to the tombstone
      * @param tmpFileLock the lock for reading/writing tmp files
      */
     public static void addTombstoneToDropBox(
-                Context ctx, File tombstone, Tombstone tombstoneProto, String processName,
+                Context ctx, File tombstone, boolean proto, String processName,
                 ReentrantLock tmpFileLock) {
         final DropBoxManager db = ctx.getSystemService(DropBoxManager.class);
         if (db == null) {
@@ -352,33 +347,31 @@
 
         // Check if we should rate limit and abort early if needed.
         DropboxRateLimiter.RateLimitResult rateLimitResult =
-                sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName);
+                sDropboxRateLimiter.shouldRateLimit(
+                        proto ? TAG_TOMBSTONE_PROTO_WITH_HEADERS : TAG_TOMBSTONE, processName);
         if (rateLimitResult.shouldRateLimit()) return;
 
         HashMap<String, Long> timestamps = readTimestamps();
         try {
-            // Remove the memory data from the proto.
-            Tombstone tombstoneProtoWithoutMemory = removeMemoryFromTombstone(tombstoneProto);
-
-            final byte[] tombstoneBytes = tombstoneProtoWithoutMemory.toByteArray();
-
-            // Use JNI to call the c++ proto to text converter and add the headers to the tombstone.
-            String tombstoneWithoutMemory = new StringBuilder(getBootHeadersToLogAndUpdate())
-                    .append(rateLimitResult.createHeader())
-                    .append(getTombstoneText(tombstoneBytes))
-                    .toString();
-
-            // Add the tombstone without memory data to dropbox.
-            db.addText(TAG_TOMBSTONE, tombstoneWithoutMemory);
-
-            // Add the tombstone proto to dropbox.
-            if (recordFileTimestamp(tombstone, timestamps)) {
-                tmpFileLock.lock();
-                try {
-                    addAugmentedProtoToDropbox(tombstone, tombstoneBytes, db, rateLimitResult);
-                } finally {
-                    tmpFileLock.unlock();
+            if (proto) {
+                if (recordFileTimestamp(tombstone, timestamps)) {
+                    // We need to attach the count indicating the number of dropped dropbox entries
+                    // due to rate limiting. Do this by enclosing the proto tombsstone in a
+                    // container proto that has the dropped entry count and the proto tombstone as
+                    // bytes (to avoid the complexity of reading and writing nested protos).
+                    tmpFileLock.lock();
+                    try {
+                        addAugmentedProtoToDropbox(tombstone, db, rateLimitResult);
+                    } finally {
+                        tmpFileLock.unlock();
+                    }
                 }
+            } else {
+                // Add the header indicating how many events have been dropped due to rate limiting.
+                final String headers = getBootHeadersToLogAndUpdate()
+                        + rateLimitResult.createHeader();
+                addFileToDropBox(db, timestamps, headers, tombstone.getPath(), LOG_SIZE,
+                                 TAG_TOMBSTONE);
             }
         } catch (IOException e) {
             Slog.e(TAG, "Can't log tombstone", e);
@@ -387,8 +380,11 @@
     }
 
     private static void addAugmentedProtoToDropbox(
-                File tombstone, byte[] tombstoneBytes, DropBoxManager db,
+                File tombstone, DropBoxManager db,
                 DropboxRateLimiter.RateLimitResult rateLimitResult) throws IOException {
+        // Read the proto tombstone file as bytes.
+        final byte[] tombstoneBytes = Files.readAllBytes(tombstone.toPath());
+
         final File tombstoneProtoWithHeaders = File.createTempFile(
                 tombstone.getName(), ".tmp", TOMBSTONE_TMP_DIR);
         Files.setPosixFilePermissions(
@@ -421,8 +417,6 @@
         }
     }
 
-    private static native String getTombstoneText(byte[] tombstoneBytes);
-
     private static void addLastkToDropBox(
             DropBoxManager db, HashMap<String, Long> timestamps,
             String headers, String footers, String filename, int maxSize,
@@ -440,31 +434,6 @@
         addFileWithFootersToDropBox(db, timestamps, headers, footers, filename, maxSize, tag);
     }
 
-    /** Removes memory information from the Tombstone proto. */
-    @VisibleForTesting
-    public static Tombstone removeMemoryFromTombstone(Tombstone tombstoneProto) {
-        Tombstone.Builder tombstoneBuilder = tombstoneProto.toBuilder()
-                .clearMemoryMappings()
-                .clearThreads()
-                .putAllThreads(tombstoneProto.getThreadsMap().entrySet()
-                        .stream()
-                        .map(BootReceiver::clearMemoryDump)
-                        .collect(Collectors.toMap(e->e.getKey(), e->e.getValue())));
-
-        if (tombstoneProto.hasSignalInfo()) {
-            tombstoneBuilder.setSignalInfo(
-                    tombstoneProto.getSignalInfo().toBuilder().clearFaultAdjacentMetadata());
-        }
-
-        return tombstoneBuilder.build();
-    }
-
-    private static AbstractMap.SimpleEntry<Integer, TombstoneProtos.Thread> clearMemoryDump(
-            Map.Entry<Integer, TombstoneProtos.Thread> e) {
-        return new AbstractMap.SimpleEntry<Integer, TombstoneProtos.Thread>(
-            e.getKey(), e.getValue().toBuilder().clearMemoryDump().build());
-    }
-
     private static void addFileToDropBox(
             DropBoxManager db, HashMap<String, Long> timestamps,
             String headers, String filename, int maxSize, String tag) throws IOException {
diff --git a/services/core/java/com/android/server/OWNERS b/services/core/java/com/android/server/OWNERS
index c258370..e289a56 100644
--- a/services/core/java/com/android/server/OWNERS
+++ b/services/core/java/com/android/server/OWNERS
@@ -27,6 +27,7 @@
 per-file **IpSec* = file:/services/core/java/com/android/server/vcn/OWNERS
 per-file *Location* = file:/services/core/java/com/android/server/location/OWNERS
 per-file *Network* = file:/services/core/java/com/android/server/net/OWNERS
+per-file *SecurityStateManager* = file:/SECURITY_STATE_OWNERS
 per-file *SoundTrigger* = file:/media/java/android/media/soundtrigger/OWNERS
 per-file *Storage* = file:/core/java/android/os/storage/OWNERS
 per-file *TimeUpdate* = file:/services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java
index 2ba3a1d..1d1e2d9 100644
--- a/services/core/java/com/android/server/VpnManagerService.java
+++ b/services/core/java/com/android/server/VpnManagerService.java
@@ -30,7 +30,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.UserInfo;
-import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.IVpnManager;
 import android.net.Network;
@@ -89,8 +88,6 @@
     private final Context mUserAllContext;
 
     private final Dependencies mDeps;
-
-    private final ConnectivityManager mCm;
     private final VpnProfileStore mVpnProfileStore;
     private final INetworkManagementService mNMS;
     private final INetd mNetd;
@@ -164,7 +161,6 @@
         mHandler = mHandlerThread.getThreadHandler();
         mVpnProfileStore = mDeps.getVpnProfileStore();
         mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
-        mCm = mContext.getSystemService(ConnectivityManager.class);
         mNMS = mDeps.getINetworkManagementService();
         mNetd = mDeps.getNetd();
         mUserManager = mContext.getSystemService(UserManager.class);
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index c436c72..fd17261 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -136,6 +136,7 @@
         "media.swcodec", // /apex/com.android.media.swcodec/bin/mediaswcodec
         "media.transcoding", // Media transcoding service
         "com.android.bluetooth",  // Bluetooth service
+        "/apex/com.android.art/bin/artd",  // ART daemon
         "/apex/com.android.os.statsd/bin/statsd",  // Stats daemon
     };
 
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index df8f17a..96b1650 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -70,7 +70,6 @@
 import static android.os.PowerExemptionManager.REASON_OPT_OUT_REQUESTED;
 import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_PLATFORM_VPN;
 import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_VPN;
-import static android.os.PowerExemptionManager.REASON_OTHER;
 import static android.os.PowerExemptionManager.REASON_PACKAGE_INSTALLER;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI;
@@ -127,7 +126,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
 import android.Manifest;
-import android.Manifest.permission;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -305,6 +303,23 @@
     @Retention(RetentionPolicy.SOURCE)
     @interface FgsStopReason {}
 
+    /**
+     * Disables foreground service background starts from BOOT_COMPLETED broadcasts for all types
+     * except:
+     * <ul>
+     *     <li>{@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_LOCATION}</li>
+     *     <li>{@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE}</li>
+     *     <li>{@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING}</li>
+     *     <li>{@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_HEALTH}</li>
+     *     <li>{@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}</li>
+     *     <li>{@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}</li>
+     * </ul>
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = VERSION_CODES.VANILLA_ICE_CREAM)
+    @Overridable
+    public static final long FGS_BOOT_COMPLETED_RESTRICTIONS = 296558535L;
+
     final ActivityManagerService mAm;
 
     // Maximum number of services that we allow to start in the background
@@ -1055,6 +1070,20 @@
         }
     }
 
+    private boolean shouldAllowBootCompletedStart(ServiceRecord r, int foregroundServiceType) {
+        @PowerExemptionManager.ReasonCode final int fgsStartReasonCode =
+                r.mInfoTempFgsAllowListReason != null ? r.mInfoTempFgsAllowListReason.mReasonCode
+                                                      : REASON_DENIED;
+        if (Flags.fgsBootCompleted()
+                && CompatChanges.isChangeEnabled(FGS_BOOT_COMPLETED_RESTRICTIONS, r.appInfo.uid)
+                && fgsStartReasonCode == PowerExemptionManager.REASON_BOOT_COMPLETED) {
+            // Filter through types
+            return ((foregroundServiceType & mAm.mConstants.FGS_BOOT_COMPLETED_ALLOWLIST) != 0);
+        }
+        // Not BOOT_COMPLETED
+        return true;
+    }
+
     private ComponentName startServiceInnerLocked(ServiceRecord r, Intent service,
             int callingUid, int callingPid, String callingProcessName,
             int callingProcessState, boolean fgRequired, boolean callerFg,
@@ -1085,7 +1114,7 @@
             // Use that as a shortcut if possible to avoid having to recheck all the conditions.
             final boolean whileInUseAllowsUiJobScheduling =
                     ActivityManagerService.doesReasonCodeAllowSchedulingUserInitiatedJobs(
-                            r.getFgsAllowWIU());
+                            r.getFgsAllowWiu_forStart());
             r.updateAllowUiJobScheduling(whileInUseAllowsUiJobScheduling
                     || mAm.canScheduleUserInitiatedJobs(callingUid, callingPid, callingPackage));
         } else {
@@ -2089,6 +2118,11 @@
                 // anyway, so we just remove the SHORT_SERVICE type.
                 foregroundServiceType &= ~FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
             }
+            if (!shouldAllowBootCompletedStart(r, foregroundServiceType)) {
+                throw new ForegroundServiceStartNotAllowedException("FGS type "
+                        + ServiceInfo.foregroundServiceTypeToLabel(foregroundServiceType)
+                        + " not allowed to start from BOOT_COMPLETED!");
+            }
 
             boolean alreadyStartedOp = false;
             boolean stopProcStatsOp = false;
@@ -2320,7 +2354,7 @@
 
                     // If the foreground service is not started from TOP process, do not allow it to
                     // have while-in-use location/camera/microphone access.
-                    if (!r.isFgsAllowedWIU()) {
+                    if (!r.isFgsAllowedWiu_forCapabilities()) {
                         Slog.w(TAG,
                                 "Foreground service started from background can not have "
                                         + "location/camera/microphone access: service "
@@ -2436,7 +2470,7 @@
                         // mAllowWhileInUsePermissionInFgs.
                         r.mAllowStartForegroundAtEntering = r.getFgsAllowStart();
                         r.mAllowWhileInUsePermissionInFgsAtEntering =
-                                r.isFgsAllowedWIU();
+                                r.isFgsAllowedWiu_forCapabilities();
                         r.mStartForegroundCount++;
                         r.mFgsEnterTime = SystemClock.uptimeMillis();
                         if (!stopProcStatsOp) {
@@ -2632,7 +2666,7 @@
                 policy.getForegroundServiceTypePolicyInfo(type, defaultToType);
         final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy(
                 mAm.mContext, r.packageName, r.app.uid, r.app.getPid(),
-                r.isFgsAllowedWIU(), policyInfo);
+                r.isFgsAllowedWiu_forStart(), policyInfo);
         RuntimeException exception = null;
         switch (code) {
             case FGS_TYPE_POLICY_CHECK_DEPRECATED: {
@@ -7580,78 +7614,76 @@
      * @param callingUid caller app's uid.
      * @param intent intent to start/bind service.
      * @param r the service to start.
-     * @param isBindService True if it's called from bindService().
+     * @param inBindService True if it's called from bindService().
      * @param forBoundFgs set to true if it's called from Service.startForeground() for a
      *                    service that's not started but bound.
-     * @return true if allow, false otherwise.
      */
     private void setFgsRestrictionLocked(String callingPackage,
             int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId,
-            BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService,
+            BackgroundStartPrivileges backgroundStartPrivileges, boolean inBindService,
             boolean forBoundFgs) {
 
-        @ReasonCode int allowWIU;
+        @ReasonCode int allowWiu;
         @ReasonCode int allowStart;
 
         // If called from bindService(), do not update the actual fields, but instead
         // keep it in a separate set of fields.
-        if (isBindService) {
-            allowWIU = r.mAllowWIUInBindService;
-            allowStart = r.mAllowStartInBindService;
+        if (inBindService) {
+            allowWiu = r.mAllowWiu_inBindService;
+            allowStart = r.mAllowStart_inBindService;
         } else {
-            allowWIU = r.mAllowWhileInUsePermissionInFgsReasonNoBinding;
-            allowStart = r.mAllowStartForegroundNoBinding;
+            allowWiu = r.mAllowWiu_noBinding;
+            allowStart = r.mAllowStart_noBinding;
         }
 
-        // Check DeviceConfig flag.
-        if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) {
-            if (allowWIU == REASON_DENIED) {
-                // BGFGS start restrictions are disabled. We're allowing while-in-use permissions.
-                // Note REASON_OTHER since there's no other suitable reason.
-                allowWIU = REASON_OTHER;
-            }
-        }
-
-        if ((allowWIU == REASON_DENIED)
-                || (allowStart == REASON_DENIED)) {
+        if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) {
             @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
                     callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges);
             // We store them to compare the old and new while-in-use logics to each other.
             // (They're not used for any other purposes.)
-            if (allowWIU == REASON_DENIED) {
-                allowWIU = allowWhileInUse;
+            if (allowWiu == REASON_DENIED) {
+                allowWiu = allowWhileInUse;
             }
             if (allowStart == REASON_DENIED) {
                 allowStart = shouldAllowFgsStartForegroundWithBindingCheckLocked(
                         allowWhileInUse, callingPackage, callingPid, callingUid, intent, r,
-                        backgroundStartPrivileges, isBindService);
+                        backgroundStartPrivileges, inBindService);
             }
         }
 
-        if (isBindService) {
-            r.mAllowWIUInBindService = allowWIU;
-            r.mAllowStartInBindService = allowStart;
+        if (inBindService) {
+            r.mAllowWiu_inBindService = allowWiu;
+            r.mAllowStart_inBindService = allowStart;
         } else {
             if (!forBoundFgs) {
-                // This is for "normal" situation.
-                r.mAllowWhileInUsePermissionInFgsReasonNoBinding = allowWIU;
-                r.mAllowStartForegroundNoBinding = allowStart;
+                // This is for "normal" situation -- either:
+                // - in Context.start[Foreground]Service()
+                // - or, in Service.startForeground() on a started service.
+                r.mAllowWiu_noBinding = allowWiu;
+                r.mAllowStart_noBinding = allowStart;
             } else {
-                // This logic is only for logging, so we only update the "by-binding" fields.
-                if (r.mAllowWIUByBindings == REASON_DENIED) {
-                    r.mAllowWIUByBindings = allowWIU;
+                // Service.startForeground() is called on a service that's not started, but bound.
+                // In this case, we set them to "byBindings", not "noBinding", because
+                // we don't want to use them when we calculate the "legacy" code.
+                //
+                // We don't want to set them to "no binding" codes, because on U-QPR1 and below,
+                // we didn't call setFgsRestrictionLocked() in the code path which sets
+                // forBoundFgs to true, and we wanted to preserve the original behavior in other
+                // places to compare the legacy and new logic.
+                if (r.mAllowWiu_byBindings == REASON_DENIED) {
+                    r.mAllowWiu_byBindings = allowWiu;
                 }
-                if (r.mAllowStartByBindings == REASON_DENIED) {
-                    r.mAllowStartByBindings = allowStart;
+                if (r.mAllowStart_byBindings == REASON_DENIED) {
+                    r.mAllowStart_byBindings = allowStart;
                 }
             }
             // Also do a binding client check, unless called from bindService().
-            if (r.mAllowWIUByBindings == REASON_DENIED) {
-                r.mAllowWIUByBindings =
+            if (r.mAllowWiu_byBindings == REASON_DENIED) {
+                r.mAllowWiu_byBindings =
                         shouldAllowFgsWhileInUsePermissionByBindingsLocked(callingUid);
             }
-            if (r.mAllowStartByBindings == REASON_DENIED) {
-                r.mAllowStartByBindings = r.mAllowWIUByBindings;
+            if (r.mAllowStart_byBindings == REASON_DENIED) {
+                r.mAllowStart_byBindings = r.mAllowWiu_byBindings;
             }
         }
     }
@@ -7660,13 +7692,13 @@
      * Reset various while-in-use and BFSL related information.
      */
     void resetFgsRestrictionLocked(ServiceRecord r) {
-        r.clearFgsAllowWIU();
+        r.clearFgsAllowWiu();
         r.clearFgsAllowStart();
 
         r.mInfoAllowStartForeground = null;
         r.mInfoTempFgsAllowListReason = null;
         r.mLoggedInfoAllowStartForeground = false;
-        r.updateAllowUiJobScheduling(r.isFgsAllowedWIU());
+        r.updateAllowUiJobScheduling(r.isFgsAllowedWiu_forStart());
     }
 
     boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) {
@@ -8284,7 +8316,8 @@
             allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering;
             fgsStartReasonCode = r.mAllowStartForegroundAtEntering;
         } else {
-            allowWhileInUsePermissionInFgs = r.isFgsAllowedWIU();
+            // TODO: Also log "forStart"
+            allowWhileInUsePermissionInFgs = r.isFgsAllowedWiu_forCapabilities();
             fgsStartReasonCode = r.getFgsAllowStart();
         }
         final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null
@@ -8306,7 +8339,7 @@
                 r.mFgsNotificationShown,
                 durationMs,
                 r.mStartForegroundCount,
-                ActivityManagerUtils.hashComponentNameForAtom(r.shortInstanceName),
+                0, // Short instance name -- no longer logging it.
                 r.mFgsHasNotificationPermission,
                 r.foregroundServiceType,
                 fgsTypeCheckCode,
@@ -8323,12 +8356,12 @@
                 mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid),
                 0,
                 0,
-                r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                r.mAllowWIUInBindService,
-                r.mAllowWIUByBindings,
-                r.mAllowStartForegroundNoBinding,
-                r.mAllowStartInBindService,
-                r.mAllowStartByBindings,
+                r.mAllowWiu_noBinding,
+                r.mAllowWiu_inBindService,
+                r.mAllowWiu_byBindings,
+                r.mAllowStart_noBinding,
+                r.mAllowStart_inBindService,
+                r.mAllowStart_byBindings,
                 fgsStartApi,
                 fgsRestrictionRecalculated);
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 3ce91c8..1c3c21c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -16,6 +16,12 @@
 
 package com.android.server.am;
 
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE;
 
@@ -73,6 +79,9 @@
             = "fgservice_screen_on_before_time";
     private static final String KEY_FGSERVICE_SCREEN_ON_AFTER_TIME
             = "fgservice_screen_on_after_time";
+
+    private static final String KEY_FGS_BOOT_COMPLETED_ALLOWLIST = "fgs_boot_completed_allowlist";
+
     private static final String KEY_CONTENT_PROVIDER_RETAIN_TIME = "content_provider_retain_time";
     private static final String KEY_GC_TIMEOUT = "gc_timeout";
     private static final String KEY_GC_MIN_INTERVAL = "gc_min_interval";
@@ -166,6 +175,15 @@
     private static final long DEFAULT_FGSERVICE_MIN_REPORT_TIME = 3*1000;
     private static final long DEFAULT_FGSERVICE_SCREEN_ON_BEFORE_TIME = 1*1000;
     private static final long DEFAULT_FGSERVICE_SCREEN_ON_AFTER_TIME = 5*1000;
+
+    private static final int DEFAULT_FGS_BOOT_COMPLETED_ALLOWLIST =
+            FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
+                | FOREGROUND_SERVICE_TYPE_HEALTH
+                | FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
+                | FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
+                | FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+                | FOREGROUND_SERVICE_TYPE_LOCATION;
+
     private static final long DEFAULT_CONTENT_PROVIDER_RETAIN_TIME = 20*1000;
     private static final long DEFAULT_GC_TIMEOUT = 5*1000;
     private static final long DEFAULT_GC_MIN_INTERVAL = 60*1000;
@@ -225,7 +243,7 @@
     /**
      * The default value to {@link #KEY_ENABLE_NEW_OOMADJ}.
      */
-    private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = Flags.oomadjusterCorrectnessRewrite();
+    private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = false;
 
     /**
      * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED}
@@ -446,6 +464,9 @@
     // on until we will stop reporting it.
     public long FGSERVICE_SCREEN_ON_AFTER_TIME = DEFAULT_FGSERVICE_SCREEN_ON_AFTER_TIME;
 
+    // Allow-list for FGS types that are allowed to start from BOOT_COMPLETED.
+    public int FGS_BOOT_COMPLETED_ALLOWLIST = DEFAULT_FGS_BOOT_COMPLETED_ALLOWLIST;
+
     // How long we will retain processes hosting content providers in the "last activity"
     // state before allowing them to drop down to the regular cached LRU list.  This is
     // to avoid thrashing of provider processes under low memory situations.
@@ -1450,6 +1471,8 @@
                     DEFAULT_FGSERVICE_SCREEN_ON_BEFORE_TIME);
             FGSERVICE_SCREEN_ON_AFTER_TIME = mParser.getLong(KEY_FGSERVICE_SCREEN_ON_AFTER_TIME,
                     DEFAULT_FGSERVICE_SCREEN_ON_AFTER_TIME);
+            FGS_BOOT_COMPLETED_ALLOWLIST = mParser.getInt(KEY_FGS_BOOT_COMPLETED_ALLOWLIST,
+                    DEFAULT_FGS_BOOT_COMPLETED_ALLOWLIST);
             CONTENT_PROVIDER_RETAIN_TIME = mParser.getLong(KEY_CONTENT_PROVIDER_RETAIN_TIME,
                     DEFAULT_CONTENT_PROVIDER_RETAIN_TIME);
             GC_TIMEOUT = mParser.getLong(KEY_GC_TIMEOUT,
@@ -2091,6 +2114,8 @@
         pw.println(FGSERVICE_SCREEN_ON_BEFORE_TIME);
         pw.print("  "); pw.print(KEY_FGSERVICE_SCREEN_ON_AFTER_TIME); pw.print("=");
         pw.println(FGSERVICE_SCREEN_ON_AFTER_TIME);
+        pw.print("  "); pw.print(KEY_FGS_BOOT_COMPLETED_ALLOWLIST); pw.print("=");
+        pw.println(FGS_BOOT_COMPLETED_ALLOWLIST);
         pw.print("  "); pw.print(KEY_CONTENT_PROVIDER_RETAIN_TIME); pw.print("=");
         pw.println(CONTENT_PROVIDER_RETAIN_TIME);
         pw.print("  "); pw.print(KEY_GC_TIMEOUT); pw.print("=");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2ee39c57..f8451fd 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -164,6 +164,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.am.MemoryStatUtil.hasMemcg;
 import static com.android.server.am.ProcessList.ProcStartHandler;
+import static com.android.server.flags.Flags.disableSystemCompaction;
 import static com.android.server.net.NetworkPolicyManagerInternal.updateBlockedReasonsWithProcState;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND;
@@ -2085,7 +2086,6 @@
                 app.makeActive(mSystemThread.getApplicationThread(), mProcessStats);
                 app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_SYSTEM);
                 addPidLocked(app);
-                mOomAdjuster.onProcessBeginLocked(app);
                 updateLruProcessLocked(app, false, null);
                 updateOomAdjLocked(OOM_ADJ_REASON_SYSTEM_INIT);
             }
@@ -4624,7 +4624,6 @@
         EventLogTags.writeAmProcBound(app.userId, pid, app.processName);
 
         synchronized (mProcLock) {
-            mOomAdjuster.onProcessBeginLocked(app);
             mOomAdjuster.setAttachingProcessStatesLSP(app);
             clearProcessForegroundLocked(app);
             app.setDebugging(false);
@@ -7104,7 +7103,6 @@
                     sdkSandboxClientAppPackage,
                     new HostingRecord(HostingRecord.HOSTING_TYPE_ADDED_APPLICATION,
                             customProcess != null ? customProcess : info.processName));
-            mOomAdjuster.onProcessBeginLocked(app);
             updateLruProcessLocked(app, false, null);
             updateOomAdjLocked(app, OOM_ADJ_REASON_PROCESS_BEGIN);
         }
@@ -8567,8 +8565,10 @@
             final long now = SystemClock.uptimeMillis();
             final long timeSinceLastIdle = now - mLastIdleTime;
 
-            // Compact all non-zygote processes to freshen up the page cache.
-            mOomAdjuster.mCachedAppOptimizer.compactAllSystem();
+            if (!disableSystemCompaction()) {
+                // Compact all non-zygote processes to freshen up the page cache.
+                mOomAdjuster.mCachedAppOptimizer.compactAllSystem();
+            }
 
             final long lowRamSinceLastIdle = mAppProfiler.getLowRamTimeSinceIdleLPr(now);
             mLastIdleTime = now;
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index ae0cd65..848a2b0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -30,6 +30,7 @@
 import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
 import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 import static android.os.Process.INVALID_UID;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
@@ -555,6 +556,13 @@
                 } else if (opt.equals("--dismiss-keyguard-if-insecure")
                       || opt.equals("--dismiss-keyguard")) {
                     mDismissKeyguardIfInsecure = true;
+                } else if (opt.equals("--allow-fgs-start-reason")) {
+                    final int reasonCode = Integer.parseInt(getNextArgRequired());
+                    mBroadcastOptions = BroadcastOptions.makeBasic();
+                    mBroadcastOptions.setTemporaryAppAllowlist(10_000,
+                            TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                            reasonCode,
+                            "");
                 } else {
                     return false;
                 }
@@ -2238,6 +2246,7 @@
 
         pw.println("Performing idle maintenance...");
         mInterface.sendIdleJobTrigger();
+        mInternal.performIdleMaintenance();
         return 0;
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerUtils.java b/services/core/java/com/android/server/am/ActivityManagerUtils.java
index 01466b8..78a2ecb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerUtils.java
+++ b/services/core/java/com/android/server/am/ActivityManagerUtils.java
@@ -129,14 +129,6 @@
     }
 
     /**
-     * @param shortInstanceName {@link ServiceRecord#shortInstanceName}.
-     * @return hash of the ServiceRecord's shortInstanceName, combined with ANDROID_ID.
-     */
-    public static int hashComponentNameForAtom(String shortInstanceName) {
-        return getUnsignedHashUnCached(shortInstanceName) ^ getAndroidIdHash();
-    }
-
-    /**
      * Helper method to log an unsafe intent event.
      */
     public static void logUnsafeIntentEvent(int event, int callingUid,
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index d0ab287..626b70b 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -275,7 +275,7 @@
     @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE =
             String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER);
     @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 10_000L;
-    @VisibleForTesting static final boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = true;
+    @VisibleForTesting static final boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = false;
     @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_ENABLED = true;
     @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4;
     @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500;
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index fc8ad6bc..b68572f 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -507,7 +507,8 @@
                 r.appInfo.uid,
                 r.shortInstanceName,
                 fgsState, // FGS State
-                r.isFgsAllowedWIU(), // allowWhileInUsePermissionInFgs
+                // TODO: Also log "forStart"
+                r.isFgsAllowedWiu_forCapabilities(), // allowWhileInUsePermissionInFgs
                 r.getFgsAllowStart(), // fgsStartReasonCode
                 r.appInfo.targetSdkVersion,
                 r.mRecentCallingUid,
@@ -518,7 +519,7 @@
                 r.mFgsNotificationShown,
                 0, // durationMs
                 r.mStartForegroundCount,
-                ActivityManagerUtils.hashComponentNameForAtom(r.shortInstanceName),
+                0, // Short instance name -- no longer logging it.
                 r.mFgsHasNotificationPermission,
                 r.foregroundServiceType,
                 0,
@@ -535,12 +536,12 @@
                 ActivityManager.PROCESS_CAPABILITY_NONE,
                 apiDurationBeforeFgsStart,
                 apiDurationAfterFgsEnd,
-                r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                r.mAllowWIUInBindService,
-                r.mAllowWIUByBindings,
-                r.mAllowStartForegroundNoBinding,
-                r.mAllowStartInBindService,
-                r.mAllowStartByBindings,
+                r.mAllowWiu_noBinding,
+                r.mAllowWiu_inBindService,
+                r.mAllowWiu_byBindings,
+                r.mAllowStart_noBinding,
+                r.mAllowStart_inBindService,
+                r.mAllowStart_byBindings,
                 FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
                 false
         );
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index b00dcd6..b507a60 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -41,7 +41,6 @@
 import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
 import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND;
-import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY;
 import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ALLOWLIST;
 import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BACKUP;
@@ -2255,7 +2254,7 @@
 
             if (s.isForeground) {
                 final int fgsType = s.foregroundServiceType;
-                if (s.isFgsAllowedWIU()) {
+                if (s.isFgsAllowedWiu_forCapabilities()) {
                     capabilityFromFGS |=
                             (fgsType & FOREGROUND_SERVICE_TYPE_LOCATION)
                                     != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0;
@@ -3494,8 +3493,8 @@
         int initialCapability =  PROCESS_CAPABILITY_NONE;
         boolean initialCached = true;
         final ProcessStateRecord state = app.mState;
-        final int prevProcState = PROCESS_STATE_UNKNOWN;
-        final int prevAdj = UNKNOWN_ADJ;
+        final int prevProcState = state.getCurRawProcState();
+        final int prevAdj = state.getCurRawAdj();
         // If the process has been marked as foreground, it is starting as the top app (with
         // Zygote#START_AS_TOP_APP_ARG), so boost the thread priority of its default UI thread.
         if (state.hasForegroundActivities()) {
@@ -3782,11 +3781,6 @@
     }
 
     @GuardedBy("mService")
-    void onProcessBeginLocked(@NonNull ProcessRecord app) {
-        // Empty, the OomAdjusterModernImpl will have an implementation.
-    }
-
-    @GuardedBy("mService")
     void onProcessEndLocked(@NonNull ProcessRecord app) {
         // Empty, the OomAdjusterModernImpl will have an implementation.
     }
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index d372108..7cc7c51 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -331,9 +331,9 @@
                 if (mLastNode[prevSlot] == node) {
                     mLastNode[prevSlot] = node.mPrev;
                 }
-                node.unlink();
             }
-            mProcessRecordNodes[newSlot].append(node);
+            // node will be firstly unlinked in the append.
+            append(node, newSlot);
         }
 
         void moveAllNodesTo(int fromSlot, int toSlot) {
@@ -389,7 +389,11 @@
         }
 
         void append(@NonNull ProcessRecord app, int targetSlot) {
-            final ProcessRecordNode node = app.mLinkedNodes[mType];
+            append(app.mLinkedNodes[mType], targetSlot);
+        }
+
+        void append(@NonNull ProcessRecordNode node, int targetSlot) {
+            node.unlink();
             mProcessRecordNodes[targetSlot].append(node);
         }
 
@@ -452,6 +456,9 @@
 
             @VisibleForTesting
             void reset() {
+                if (HEAD.mNext != TAIL) {
+                    HEAD.mNext.mPrev = TAIL.mPrev.mNext = null;
+                }
                 HEAD.mNext = TAIL;
                 TAIL.mPrev = HEAD;
             }
@@ -558,20 +565,6 @@
 
     @GuardedBy("mService")
     @Override
-    void onProcessBeginLocked(@NonNull ProcessRecord app) {
-        // Check one type should be good enough.
-        if (app.mLinkedNodes[ProcessRecordNode.NODE_TYPE_PROC_STATE] == null) {
-            for (int i = 0; i < app.mLinkedNodes.length; i++) {
-                app.mLinkedNodes[i] = new ProcessRecordNode(app);
-            }
-        }
-        if (!app.mLinkedNodes[ProcessRecordNode.NODE_TYPE_PROC_STATE].isLinked()) {
-            linkProcessRecordToList(app);
-        }
-    }
-
-    @GuardedBy("mService")
-    @Override
     void onProcessEndLocked(@NonNull ProcessRecord app) {
         if (app.mLinkedNodes[ProcessRecordNode.NODE_TYPE_PROC_STATE] != null
                 && app.mLinkedNodes[ProcessRecordNode.NODE_TYPE_PROC_STATE].isLinked()) {
diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
index 40b1de6..1412259 100644
--- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
@@ -299,6 +299,11 @@
         SparseBooleanArray lastPids = new SparseBooleanArray(20);
         ActivityManagerService.VolatileDropboxEntryStates volatileDropboxEntriyStates = null;
 
+        if (mApp.isDebugging()) {
+            Slog.i(TAG, "Skipping debugged app ANR: " + this + " " + annotation);
+            return;
+        }
+
         mApp.getWindowProcessController().appEarlyNotResponding(annotation, () -> {
             latencyTracker.waitingOnAMSLockStarted();
             synchronized (mService) {
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 3156e9d..9a9d4ee 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2153,6 +2153,7 @@
                     mService.forceStopPackageLocked(app.info.packageName,
                             UserHandle.getAppId(app.uid),
                             false, false, true, false, false, false, app.userId, "start failure");
+                    app.doEarlyCleanupIfNecessaryLocked();
                 }
             }
         };
@@ -2481,7 +2482,6 @@
                         app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                         app.info.dataDir, null, app.info.packageName,
                         app.getDisabledCompatChanges(),
-                        bindOverrideSysprops,
                         new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
             } else if (hostingRecord.usesAppZygote()) {
                 final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);
@@ -2782,6 +2782,7 @@
             }
             noteAppKill(app, ApplicationExitInfo.REASON_OTHER,
                     ApplicationExitInfo.SUBREASON_INVALID_START, reason);
+            app.doEarlyCleanupIfNecessaryLocked();
             return false;
         }
         mService.mBatteryStatsService.noteProcessStart(app.processName, app.info.uid);
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index b4cd6a3..e5c4a66 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -621,6 +621,34 @@
         mWindowProcessController = new WindowProcessController(
                 mService.mActivityTaskManager, info, processName, uid, userId, this, this);
         mPkgList.put(_info.packageName, new ProcessStats.ProcessStateHolder(_info.longVersionCode));
+        updateProcessRecordNodes(this);
+    }
+
+    /**
+     * Helper function to let test cases update the pointers.
+     */
+    @VisibleForTesting
+    static void updateProcessRecordNodes(@NonNull ProcessRecord app) {
+        if (app.mService.mConstants.ENABLE_NEW_OOMADJ) {
+            for (int i = 0; i < app.mLinkedNodes.length; i++) {
+                app.mLinkedNodes[i] = new ProcessRecordNode(app);
+            }
+        }
+    }
+
+    /**
+     * Perform cleanups if the process record is going to be discarded in an early
+     * stage of the process lifecycle, specifically when the process has not even
+     * attached itself to the system_server.
+     */
+    @GuardedBy("mService")
+    void doEarlyCleanupIfNecessaryLocked() {
+        if (getThread() == null) {
+            // It's not even attached, make sure we unlink its process nodes.
+            mService.mOomAdjuster.onProcessEndLocked(this);
+        } else {
+            // Let the binder died callback handle the cleanup.
+        }
     }
 
     void resetCrashingOnRestart() {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 0fba739..08b129e 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -37,6 +37,9 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -81,6 +84,37 @@
     // Maximum number of times it can fail during execution before giving up.
     static final int MAX_DONE_EXECUTING_COUNT = 6;
 
+
+    // Compat IDs for the new FGS logic. For now, we just disable all of them.
+    // TODO: Enable them at some point, but only for V+ builds.
+
+    /**
+     * Compat ID to enable the new FGS start logic, for permission calculation used for
+     * per-FGS-type eligibility calculation.
+     * (See also android.app.ForegroundServiceTypePolicy)
+     */
+    @ChangeId
+    // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Disabled
+    static final long USE_NEW_WIU_LOGIC_FOR_START = 311208629L;
+
+    /**
+     * Compat ID to enable the new FGS start logic, for capability calculation.
+     */
+    @ChangeId
+    // Always enabled
+    @Disabled
+    static final long USE_NEW_WIU_LOGIC_FOR_CAPABILITIES = 313677553L;
+
+    /**
+     * Compat ID to enable the new FGS start logic, for deciding whether to allow FGS start from
+     * the background.
+     */
+    @ChangeId
+    // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Disabled
+    static final long USE_NEW_BFSL_LOGIC = 311208749L;
+
     final ActivityManagerService ams;
     final ComponentName name; // service component.
     final ComponentName instanceName; // service component's per-instance name.
@@ -178,7 +212,7 @@
     // If it's not DENIED, while-in-use permissions are allowed.
     // while-in-use permissions in FGS started from background might be restricted.
     @PowerExemptionManager.ReasonCode
-    int mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
+    int mAllowWiu_noBinding = REASON_DENIED;
 
     // A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state.
     boolean mAllowWhileInUsePermissionInFgsAtEntering;
@@ -208,7 +242,7 @@
     // allow the service becomes foreground service? Service started from background may not be
     // allowed to become a foreground service.
     @PowerExemptionManager.ReasonCode
-    int mAllowStartForegroundNoBinding = REASON_DENIED;
+    int mAllowStart_noBinding = REASON_DENIED;
     // A copy of mAllowStartForeground's value when the service is entering FGS state.
     @PowerExemptionManager.ReasonCode
     int mAllowStartForegroundAtEntering = REASON_DENIED;
@@ -220,72 +254,172 @@
     boolean mLoggedInfoAllowStartForeground;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowWIUInBindService = REASON_DENIED;
+    int mAllowWiu_inBindService = REASON_DENIED;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowWIUByBindings = REASON_DENIED;
+    int mAllowWiu_byBindings = REASON_DENIED;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowStartInBindService = REASON_DENIED;
+    int mAllowStart_inBindService = REASON_DENIED;
 
     @PowerExemptionManager.ReasonCode
-    int mAllowStartByBindings = REASON_DENIED;
+    int mAllowStart_byBindings = REASON_DENIED;
 
-    @PowerExemptionManager.ReasonCode
-    int getFgsAllowWIU() {
-        return mAllowWhileInUsePermissionInFgsReasonNoBinding != REASON_DENIED
-                ? mAllowWhileInUsePermissionInFgsReasonNoBinding
-                : mAllowWIUInBindService;
+    /**
+     * Whether to use the new "while-in-use permission" logic for FGS start
+     */
+    private boolean useNewWiuLogic_forStart() {
+        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
+                && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
     }
 
-    boolean isFgsAllowedWIU() {
-        return getFgsAllowWIU() != REASON_DENIED;
+    /**
+     * Whether to use the new "while-in-use permission" logic for capabilities
+     */
+    private boolean useNewWiuLogic_forCapabilities() {
+        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
+                && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
     }
 
+    /**
+     * Whether to use the new "FGS BG start exemption" logic.
+     */
+    private boolean useNewBfslLogic() {
+        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
+                && CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
+    }
+
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowWiu_legacy() {
+        // In the legacy mode (U-), we use mAllowWiu_inBindService and mAllowWiu_noBinding.
+        return reasonOr(
+                mAllowWiu_noBinding,
+                mAllowWiu_inBindService
+        );
+    }
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowWiu_new() {
+        // In the new mode, use by-bindings instead of in-bind-service
+        return reasonOr(
+                mAllowWiu_noBinding,
+                mAllowWiu_byBindings
+        );
+    }
+
+    /**
+     * We use this logic for ForegroundServiceTypePolicy and UIDT eligibility check.
+     */
+    @PowerExemptionManager.ReasonCode
+    int getFgsAllowWiu_forStart() {
+        if (useNewWiuLogic_forStart()) {
+            return getFgsAllowWiu_new();
+        } else {
+            return getFgsAllowWiu_legacy();
+        }
+    }
+
+    /**
+     * We use this logic for the capability calculation in oom-adjuster.
+     */
+    @PowerExemptionManager.ReasonCode
+    int getFgsAllowWiu_forCapabilities() {
+        if (useNewWiuLogic_forCapabilities()) {
+            return getFgsAllowWiu_new();
+        } else {
+            // If alwaysUseNewLogicForWiuCapabilities() isn't set, just use the same logic as
+            // getFgsAllowWiu_forStart().
+            return getFgsAllowWiu_forStart();
+        }
+    }
+
+    /**
+     * We use this logic for ForegroundServiceTypePolicy and UIDT eligibility check.
+     */
+    boolean isFgsAllowedWiu_forStart() {
+        return getFgsAllowWiu_forStart() != REASON_DENIED;
+    }
+
+    /**
+     * We use this logic for the capability calculation in oom-adjuster.
+     */
+    boolean isFgsAllowedWiu_forCapabilities() {
+        return getFgsAllowWiu_forCapabilities() != REASON_DENIED;
+    }
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowStart_legacy() {
+        // This is used for BFSL (background FGS launch) exemption.
+        // Originally -- on U-QPR1 and before -- we only used [in-bind-service] + [no-binding].
+        // This would exclude certain "valid" situations, so in U-QPR2, we started
+        // using [by-bindings] too.
+        return reasonOr(
+                mAllowStart_noBinding,
+                mAllowStart_inBindService,
+                mAllowStart_byBindings
+        );
+    }
+
+    @PowerExemptionManager.ReasonCode
+    private int getFgsAllowStart_new() {
+        // In the new mode, we don't use [in-bind-service].
+        return reasonOr(
+                mAllowStart_noBinding,
+                mAllowStart_byBindings
+        );
+    }
+
+    /**
+     * Calculate a BFSL exemption code.
+     */
     @PowerExemptionManager.ReasonCode
     int getFgsAllowStart() {
-        return mAllowStartForegroundNoBinding != REASON_DENIED
-                ? mAllowStartForegroundNoBinding
-                : (mAllowStartByBindings != REASON_DENIED
-                ? mAllowStartByBindings
-                : mAllowStartInBindService);
+        if (useNewBfslLogic()) {
+            return getFgsAllowStart_new();
+        } else {
+            return getFgsAllowStart_legacy();
+        }
     }
 
+    /**
+     * Return whether BFSL is allowed or not.
+     */
     boolean isFgsAllowedStart() {
         return getFgsAllowStart() != REASON_DENIED;
     }
 
-    void clearFgsAllowWIU() {
-        mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
-        mAllowWIUInBindService = REASON_DENIED;
-        mAllowWIUByBindings = REASON_DENIED;
+    void clearFgsAllowWiu() {
+        mAllowWiu_noBinding = REASON_DENIED;
+        mAllowWiu_inBindService = REASON_DENIED;
+        mAllowWiu_byBindings = REASON_DENIED;
     }
 
     void clearFgsAllowStart() {
-        mAllowStartForegroundNoBinding = REASON_DENIED;
-        mAllowStartInBindService = REASON_DENIED;
-        mAllowStartByBindings = REASON_DENIED;
+        mAllowStart_noBinding = REASON_DENIED;
+        mAllowStart_inBindService = REASON_DENIED;
+        mAllowStart_byBindings = REASON_DENIED;
     }
 
     @PowerExemptionManager.ReasonCode
-    int reasonOr(@PowerExemptionManager.ReasonCode int first,
+    static int reasonOr(
+            @PowerExemptionManager.ReasonCode int first,
             @PowerExemptionManager.ReasonCode int second) {
         return first != REASON_DENIED ? first : second;
     }
 
-    boolean allowedChanged(@PowerExemptionManager.ReasonCode int first,
-            @PowerExemptionManager.ReasonCode int second) {
-        return (first == REASON_DENIED) != (second == REASON_DENIED);
+    @PowerExemptionManager.ReasonCode
+    static int reasonOr(
+            @PowerExemptionManager.ReasonCode int first,
+            @PowerExemptionManager.ReasonCode int second,
+            @PowerExemptionManager.ReasonCode int third) {
+        return first != REASON_DENIED ? first : reasonOr(second, third);
     }
 
-    String changeMessage(@PowerExemptionManager.ReasonCode int first,
-            @PowerExemptionManager.ReasonCode int second) {
-        return reasonOr(first, second) == REASON_DENIED ? "DENIED"
-                : ("ALLOWED ("
-                + reasonCodeToString(first)
-                + "+"
-                + reasonCodeToString(second)
-                + ")");
+    boolean allowedChanged(
+            @PowerExemptionManager.ReasonCode int legacyCode,
+            @PowerExemptionManager.ReasonCode int newCode) {
+        return (legacyCode == REASON_DENIED) != (newCode == REASON_DENIED);
     }
 
     private String getFgsInfoForWtf() {
@@ -295,15 +429,14 @@
     }
 
     void maybeLogFgsLogicChange() {
-        final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                mAllowWIUInBindService);
-        final int newWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                mAllowWIUByBindings);
-        final int origStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartInBindService);
-        final int newStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+        final int wiuLegacy = getFgsAllowWiu_legacy();
+        final int wiuNew = getFgsAllowWiu_new();
 
-        final boolean wiuChanged = allowedChanged(origWiu, newWiu);
-        final boolean startChanged = allowedChanged(origStart, newStart);
+        final int startLegacy = getFgsAllowStart_legacy();
+        final int startNew = getFgsAllowStart_new();
+
+        final boolean wiuChanged = allowedChanged(wiuLegacy, wiuNew);
+        final boolean startChanged = allowedChanged(startLegacy, startNew);
 
         if (!wiuChanged && !startChanged) {
             return;
@@ -311,15 +444,14 @@
         final String message = "FGS logic changed:"
                 + (wiuChanged ? " [WIU changed]" : "")
                 + (startChanged ? " [BFSL changed]" : "")
-                + " OW:" // Orig-WIU
-                + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding,
-                        mAllowWIUInBindService)
-                + " NW:" // New-WIU
-                + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUByBindings)
-                + " OS:" // Orig-start
-                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService)
-                + " NS:" // New-start
-                + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings)
+                + " Orig WIU:"
+                + reasonCodeToString(wiuLegacy)
+                + " New WIU:"
+                + reasonCodeToString(wiuNew)
+                + " Orig BFSL:"
+                + reasonCodeToString(startLegacy)
+                + " New BFSL:"
+                + reasonCodeToString(startNew)
                 + getFgsInfoForWtf();
         Slog.wtf(TAG_SERVICE, message);
     }
@@ -587,6 +719,7 @@
                 proto.write(ServiceRecordProto.AppInfo.RES_DIR, appInfo.publicSourceDir);
             }
             proto.write(ServiceRecordProto.AppInfo.DATA_DIR, appInfo.dataDir);
+            proto.write(ServiceRecordProto.AppInfo.TARGET_SDK_VERSION, appInfo.targetSdkVersion);
             proto.end(appInfoToken);
         }
         if (app != null) {
@@ -611,8 +744,10 @@
         ProtoUtils.toDuration(proto, ServiceRecordProto.LAST_ACTIVITY_TIME, lastActivity, now);
         ProtoUtils.toDuration(proto, ServiceRecordProto.RESTART_TIME, restartTime, now);
         proto.write(ServiceRecordProto.CREATED_FROM_FG, createdFromFg);
+
+        // TODO: Log "forStart" too.
         proto.write(ServiceRecordProto.ALLOW_WHILE_IN_USE_PERMISSION_IN_FGS,
-                isFgsAllowedWIU());
+                isFgsAllowedWiu_forCapabilities());
 
         if (startRequested || delayedStop || lastStartId != 0) {
             long startToken = proto.start(ServiceRecordProto.START);
@@ -691,15 +826,25 @@
             proto.end(shortFgsToken);
         }
 
+        // TODO: Dump all the mAllowWiu* and mAllowStart* fields as needed.
+
         proto.end(token);
     }
 
+    void dumpReasonCode(PrintWriter pw, String prefix, String fieldName, int code) {
+        pw.print(prefix);
+        pw.print(fieldName);
+        pw.print("=");
+        pw.println(PowerExemptionManager.reasonCodeToString(code));
+    }
+
     void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("intent={");
                 pw.print(intent.getIntent().toShortString(false, true, false, false));
                 pw.println('}');
         pw.print(prefix); pw.print("packageName="); pw.println(packageName);
         pw.print(prefix); pw.print("processName="); pw.println(processName);
+        pw.print(prefix); pw.print("targetSdkVersion="); pw.println(appInfo.targetSdkVersion);
         if (permission != null) {
             pw.print(prefix); pw.print("permission="); pw.println(permission);
         }
@@ -727,26 +872,38 @@
             pw.print(prefix); pw.print("mIsAllowedBgActivityStartsByStart=");
             pw.println(mBackgroundStartPrivilegesByStartMerged);
         }
-        pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason=");
-        pw.println(PowerExemptionManager.reasonCodeToString(
-                mAllowWhileInUsePermissionInFgsReasonNoBinding));
 
-        pw.print(prefix); pw.print("mAllowWIUInBindService=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUInBindService));
-        pw.print(prefix); pw.print("mAllowWIUByBindings=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUByBindings));
+        pw.print(prefix); pw.print("useNewWiuLogic_forCapabilities()=");
+        pw.println(useNewWiuLogic_forCapabilities());
+        pw.print(prefix); pw.print("useNewWiuLogic_forStart()=");
+        pw.println(useNewWiuLogic_forStart());
+        pw.print(prefix); pw.print("useNewBfslLogic()=");
+        pw.println(useNewBfslLogic());
+
+        dumpReasonCode(pw, prefix, "mAllowWiu_noBinding", mAllowWiu_noBinding);
+        dumpReasonCode(pw, prefix, "mAllowWiu_inBindService", mAllowWiu_inBindService);
+        dumpReasonCode(pw, prefix, "mAllowWiu_byBindings", mAllowWiu_byBindings);
+
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_legacy", getFgsAllowWiu_legacy());
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_new", getFgsAllowWiu_new());
+
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_forStart", getFgsAllowWiu_forStart());
+        dumpReasonCode(pw, prefix, "getFgsAllowWiu_forCapabilities",
+                getFgsAllowWiu_forCapabilities());
 
         pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling);
         pw.print(prefix); pw.print("recentCallingPackage=");
                 pw.println(mRecentCallingPackage);
         pw.print(prefix); pw.print("recentCallingUid=");
         pw.println(mRecentCallingUid);
-        pw.print(prefix); pw.print("allowStartForeground=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForegroundNoBinding));
-        pw.print(prefix); pw.print("mAllowStartInBindService=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartInBindService));
-        pw.print(prefix); pw.print("mAllowStartByBindings=");
-        pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartByBindings));
+
+        dumpReasonCode(pw, prefix, "mAllowStart_noBinding", mAllowStart_noBinding);
+        dumpReasonCode(pw, prefix, "mAllowStart_inBindService", mAllowStart_inBindService);
+        dumpReasonCode(pw, prefix, "mAllowStart_byBindings", mAllowStart_byBindings);
+
+        dumpReasonCode(pw, prefix, "getFgsAllowStart_legacy", getFgsAllowStart_legacy());
+        dumpReasonCode(pw, prefix, "getFgsAllowStart_new", getFgsAllowStart_new());
+        dumpReasonCode(pw, prefix, "getFgsAllowStart", getFgsAllowStart());
 
         pw.print(prefix); pw.print("startForegroundCount=");
         pw.println(mStartForegroundCount);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 3e1edf2..d0d647c 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -141,6 +141,7 @@
         "context_hub",
         "core_experiments_team_internal",
         "core_graphics",
+        "core_libraries",
         "dck_framework",
         "devoptions_settings",
         "game",
@@ -178,6 +179,7 @@
         "text",
         "threadnetwork",
         "tv_system_ui",
+        "usb",
         "vibrator",
         "virtual_devices",
         "wallet_integration",
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index a6b532c..2b81dbc 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -2059,9 +2059,6 @@
             mTargetUserId = targetUserId;
             userSwitchUiEnabled = mUserSwitchUiEnabled;
         }
-        if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) {
-            mInjector.setHasTopUi(true);
-        }
         if (userSwitchUiEnabled) {
             UserInfo currentUserInfo = getUserInfo(currentUserId);
             Pair<UserInfo, UserInfo> userNames = new Pair<>(currentUserInfo, targetUserInfo);
@@ -2130,9 +2127,6 @@
     }
 
     private void endUserSwitch() {
-        if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) {
-            mInjector.setHasTopUi(false);
-        }
         final int nextUserId;
         synchronized (mLock) {
             nextUserId = ObjectUtils.getOrElse(mPendingTargetUserIds.poll(), UserHandle.USER_NULL);
@@ -3050,8 +3044,8 @@
 
     /**
      * Returns whether the given user requires credential entry at this time. This is used to
-     * intercept activity launches for locked work apps due to work challenge being triggered
-     * or when the profile user is yet to be unlocked.
+     * intercept activity launches for apps corresponding to locked profiles due to separate
+     * challenge being triggered or when the profile user is yet to be unlocked.
      */
     protected boolean shouldConfirmCredentials(@UserIdInt int userId) {
         if (getStartedUserState(userId) == null) {
@@ -3816,15 +3810,6 @@
             getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId);
         }
 
-        void setHasTopUi(boolean hasTopUi) {
-            try {
-                Slogf.i(TAG, "Setting hasTopUi to " + hasTopUi);
-                mService.setHasTopUi(hasTopUi);
-            } catch (RemoteException e) {
-                Slogf.e(TAG, "Failed to allow using all CPU cores", e);
-            }
-        }
-
         void onSystemUserVisibilityChanged(boolean visible) {
             getUserManagerInternal().onSystemUserVisibilityChanged(visible);
         }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index d9e8ddd..654aebd 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -28,3 +28,10 @@
     description: "Restrict network access for certain applications in BFGS process state"
     bug: "304347838"
 }
+# Whether to use the new while-in-use / BG-FGS-start logic
+flag {
+     namespace: "backstage_power"
+     name: "new_fgs_restriction_logic"
+     description: "Enable the new FGS restriction logic"
+     bug: "276963716"
+}
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index b3fb9c9..8b7e56e 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -20,6 +20,7 @@
 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 import static android.content.Intent.EXTRA_REPLACING;
 import static android.server.app.Flags.gameDefaultFrameRate;
+import static android.server.app.Flags.disableGameModeWhenAppTop;
 
 import static com.android.internal.R.styleable.GameModeConfig_allowGameAngleDriver;
 import static com.android.internal.R.styleable.GameModeConfig_allowGameDownscaling;
@@ -181,7 +182,9 @@
     @Nullable
     final MyUidObserver mUidObserver;
     @GuardedBy("mUidObserverLock")
-    private final Set<Integer> mForegroundGameUids = new HashSet<>();
+    private final Set<Integer> mGameForegroundUids = new HashSet<>();
+    @GuardedBy("mUidObserverLock")
+    private final Set<Integer> mNonGameForegroundUids = new HashSet<>();
     private final GameManagerServiceSystemPropertiesWrapper mSysProps;
     private float mGameDefaultFrameRateValue;
 
@@ -238,12 +241,10 @@
                 FileUtils.S_IRUSR | FileUtils.S_IWUSR
                         | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
                 -1, -1);
-        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
+        if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
             mGameServiceController = new GameServiceController(
                     context, BackgroundThread.getExecutor(),
-                    new GameServiceProviderSelectorImpl(
-                            context.getResources(),
-                            context.getPackageManager()),
+                    new GameServiceProviderSelectorImpl(context.getResources(), mPackageManager),
                     new GameServiceProviderInstanceFactoryImpl(context));
         } else {
             mGameServiceController = null;
@@ -2245,7 +2246,7 @@
 
         // Update all foreground games' frame rate.
         synchronized (mUidObserverLock) {
-            for (int uid : mForegroundGameUids) {
+            for (int uid : mGameForegroundUids) {
                 setGameDefaultFrameRateOverride(uid, getGameDefaultFrameRate(isEnabled));
             }
         }
@@ -2261,31 +2262,44 @@
         @Override
         public void onUidGone(int uid, boolean disabled) {
             synchronized (mUidObserverLock) {
-                disableGameMode(uid);
+                handleUidMovedOffTop(uid);
             }
         }
 
         @Override
         public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
+            switch (procState) {
+                case ActivityManager.PROCESS_STATE_TOP:
+                    handleUidMovedToTop(uid);
+                    return;
+                default:
+                    handleUidMovedOffTop(uid);
+            }
+        }
+
+        private void handleUidMovedToTop(int uid) {
+            final String[] packages = mPackageManager.getPackagesForUid(uid);
+            if (packages == null || packages.length == 0) {
+                return;
+            }
+
+            final int userId = mContext.getUserId();
+            final boolean isNotGame = Arrays.stream(packages).noneMatch(
+                    p -> isPackageGame(p, userId));
             synchronized (mUidObserverLock) {
-
-                if (procState != ActivityManager.PROCESS_STATE_TOP) {
-                    disableGameMode(uid);
+                if (isNotGame) {
+                    if (disableGameModeWhenAppTop()) {
+                        if (!mGameForegroundUids.isEmpty() && mNonGameForegroundUids.isEmpty()) {
+                            Slog.v(TAG, "Game power mode OFF (first non-game in foreground)");
+                            mPowerManagerInternal.setPowerMode(Mode.GAME, false);
+                        }
+                        mNonGameForegroundUids.add(uid);
+                    }
                     return;
                 }
-
-                final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
-                if (packages == null || packages.length == 0) {
-                    return;
-                }
-
-                final int userId = mContext.getUserId();
-                if (!Arrays.stream(packages).anyMatch(p -> isPackageGame(p, userId))) {
-                    return;
-                }
-
-                if (mForegroundGameUids.isEmpty()) {
-                    Slog.v(TAG, "Game power mode ON (process state was changed to foreground)");
+                if (mGameForegroundUids.isEmpty() && (!disableGameModeWhenAppTop()
+                        || mNonGameForegroundUids.isEmpty())) {
+                    Slog.v(TAG, "Game power mode ON (first game in foreground)");
                     mPowerManagerInternal.setPowerMode(Mode.GAME, true);
                 }
                 final boolean isGameDefaultFrameRateDisabled =
@@ -2293,22 +2307,26 @@
                                 PROPERTY_DEBUG_GFX_GAME_DEFAULT_FRAME_RATE_DISABLED, false);
                 setGameDefaultFrameRateOverride(uid,
                         getGameDefaultFrameRate(!isGameDefaultFrameRateDisabled));
-                mForegroundGameUids.add(uid);
+                mGameForegroundUids.add(uid);
             }
         }
 
-        private void disableGameMode(int uid) {
+        private void handleUidMovedOffTop(int uid) {
             synchronized (mUidObserverLock) {
-                if (!mForegroundGameUids.contains(uid)) {
-                    return;
+                if (mGameForegroundUids.contains(uid)) {
+                    mGameForegroundUids.remove(uid);
+                    if (mGameForegroundUids.isEmpty() && (!disableGameModeWhenAppTop()
+                            || mNonGameForegroundUids.isEmpty())) {
+                        Slog.v(TAG, "Game power mode OFF (no games in foreground)");
+                        mPowerManagerInternal.setPowerMode(Mode.GAME, false);
+                    }
+                } else if (disableGameModeWhenAppTop() && mNonGameForegroundUids.contains(uid)) {
+                    mNonGameForegroundUids.remove(uid);
+                    if (mNonGameForegroundUids.isEmpty() && !mGameForegroundUids.isEmpty()) {
+                        Slog.v(TAG, "Game power mode ON (only games in foreground)");
+                        mPowerManagerInternal.setPowerMode(Mode.GAME, true);
+                    }
                 }
-                mForegroundGameUids.remove(uid);
-                if (!mForegroundGameUids.isEmpty()) {
-                    return;
-                }
-                Slog.v(TAG,
-                        "Game power mode OFF (process remomved or state changed to background)");
-                mPowerManagerInternal.setPowerMode(Mode.GAME, false);
             }
         }
     }
diff --git a/services/core/java/com/android/server/app/flags.aconfig b/services/core/java/com/android/server/app/flags.aconfig
index f2e4783..0673013 100644
--- a/services/core/java/com/android/server/app/flags.aconfig
+++ b/services/core/java/com/android/server/app/flags.aconfig
@@ -6,4 +6,11 @@
     description: "This flag guards the new behavior with the addition of Game Default Frame Rate feature."
     bug: "286084594"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+    name: "disable_game_mode_when_app_top"
+    namespace: "game"
+    description: "Disable game power mode when a non-game app is also top and visible"
+    bug: "299295925"
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 8091753..dada72e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -56,6 +56,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.util.PrintWriterPrinter;
 
 import com.android.internal.annotations.GuardedBy;
@@ -1640,7 +1641,7 @@
         return mBtHelper.getLeAudioDeviceGroupId(device);
     }
 
-    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+    /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) {
         return mBtHelper.getLeAudioGroupAddresses(groupId);
     }
 
@@ -2651,9 +2652,9 @@
         }
     }
 
-    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+    List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) {
         synchronized (mDeviceStateLock) {
-            return mDeviceInventory.getDeviceAddresses(device);
+            return mDeviceInventory.getDeviceIdentityAddresses(device);
         }
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index d138f24..e05824a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -566,23 +566,40 @@
         final int mDeviceType;
         final @NonNull String mDeviceName;
         final @NonNull String mDeviceAddress;
+        @NonNull String mDeviceIdentityAddress;
         int mDeviceCodecFormat;
-        @NonNull String mPeerDeviceAddress;
         final int mGroupId;
+        @NonNull String mPeerDeviceAddress;
+        @NonNull String mPeerIdentityDeviceAddress;
 
         /** Disabled operating modes for this device. Use a negative logic so that by default
          * an empty list means all modes are allowed.
          * See BluetoothAdapter.AUDIO_MODE_DUPLEX and BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY */
         @NonNull ArraySet<String> mDisabledModes = new ArraySet(0);
 
-        DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat, String peerDeviceAddress, int groupId) {
+        DeviceInfo(int deviceType, String deviceName, String address,
+                   String identityAddress, int codecFormat,
+                   int groupId, String peerAddress, String peerIdentityAddress) {
             mDeviceType = deviceType;
-            mDeviceName = deviceName == null ? "" : deviceName;
-            mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
-            mDeviceCodecFormat = deviceCodecFormat;
-            mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress;
+            mDeviceName = TextUtils.emptyIfNull(deviceName);
+            mDeviceAddress = TextUtils.emptyIfNull(address);
+            mDeviceIdentityAddress = TextUtils.emptyIfNull(identityAddress);
+            mDeviceCodecFormat = codecFormat;
             mGroupId = groupId;
+            mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress);
+            mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress);
+        }
+
+        /** Constructor for all devices except A2DP sink and LE Audio */
+        DeviceInfo(int deviceType, String deviceName, String address) {
+            this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT);
+        }
+
+        /** Constructor for A2DP sink devices */
+        DeviceInfo(int deviceType, String deviceName, String address,
+                   String identityAddress, int codecFormat) {
+            this(deviceType, deviceName, address, identityAddress, codecFormat,
+                    BluetoothLeAudio.GROUP_ID_INVALID, null, null);
         }
 
         void setModeDisabled(String mode) {
@@ -601,25 +618,20 @@
             return isModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX);
         }
 
-        DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat) {
-            this(deviceType, deviceName, deviceAddress, deviceCodecFormat,
-                    null, BluetoothLeAudio.GROUP_ID_INVALID);
-        }
-
-        DeviceInfo(int deviceType, String deviceName, String deviceAddress) {
-            this(deviceType, deviceName, deviceAddress, AudioSystem.AUDIO_FORMAT_DEFAULT);
-        }
-
         @Override
         public String toString() {
             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
                     + " (" + AudioSystem.getDeviceName(mDeviceType)
                     + ") name:" + mDeviceName
                     + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress)
+                    + " identity addr:"
+                    + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceIdentityAddress)
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
-                    + " peer addr:" + mPeerDeviceAddress
                     + " group:" + mGroupId
+                    + " peer addr:"
+                    + Utils.anonymizeBluetoothAddress(mDeviceType, mPeerDeviceAddress)
+                    + " peer identity addr:"
+                    + Utils.anonymizeBluetoothAddress(mDeviceType, mPeerIdentityDeviceAddress)
                     + " disabled modes: " + mDisabledModes + "]";
         }
 
@@ -947,20 +959,44 @@
     }
 
 
+    /**
+     * Goes over all connected LE Audio devices in the provided group ID and
+     * update:
+     * - the peer address according to the addres of other device in the same
+     * group (can also clear the peer address is not anymore in the group)
+     * - The dentity address if not yet set.
+     * LE Audio buds in a pair are in the same group.
+     * @param groupId the LE Audio group to update
+     */
     /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) {
         synchronized (mDevicesLock) {
+            // <address, identy address>
+            List<Pair<String, String>> addresses = new ArrayList<>();
             for (DeviceInfo di : mConnectedDevices.values()) {
                 if (di.mGroupId == groupId) {
-                    List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                    if (addresses.isEmpty()) {
+                        addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                    }
                     if (di.mPeerDeviceAddress.equals("")) {
-                        for (String addr : addresses) {
-                            if (!addr.equals(di.mDeviceAddress)) {
-                                di.mPeerDeviceAddress = addr;
+                        for (Pair<String, String> addr : addresses) {
+                            if (!addr.first.equals(di.mDeviceAddress)) {
+                                di.mPeerDeviceAddress = addr.first;
+                                di.mPeerIdentityDeviceAddress = addr.second;
                                 break;
                             }
                         }
-                    } else if (!addresses.contains(di.mPeerDeviceAddress)) {
+                    } else if (!addresses.contains(
+                            new Pair(di.mPeerDeviceAddress, di.mPeerIdentityDeviceAddress))) {
                         di.mPeerDeviceAddress = "";
+                        di.mPeerIdentityDeviceAddress = "";
+                    }
+                    if (di.mDeviceIdentityAddress.equals("")) {
+                        for (Pair<String, String> addr : addresses) {
+                            if (addr.first.equals(di.mDeviceAddress)) {
+                                di.mDeviceIdentityAddress = addr.second;
+                                break;
+                            }
+                        }
                     }
                 }
             }
@@ -1964,7 +2000,7 @@
         mDeviceBroker.clearA2dpSuspended(true /* internalOnly */);
 
         final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
-                address, codec);
+                address, btInfo.mDevice.getIdentityAddress(), codec);
         final String diKey = di.getKey();
         mConnectedDevices.put(diKey, di);
         // on a connection always overwrite the device seen by AudioPolicy, see comment above when
@@ -2381,12 +2417,15 @@
             // Find LE Group ID and peer headset address if available
             final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice);
             String peerAddress = "";
+            String peerIdentityAddress = "";
             if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
-                List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                List<Pair<String, String>> addresses =
+                        mDeviceBroker.getLeAudioGroupAddresses(groupId);
                 if (addresses.size() > 1) {
-                    for (String addr : addresses) {
-                        if (!addr.equals(address)) {
-                            peerAddress = addr;
+                    for (Pair<String, String> addr : addresses) {
+                        if (!addr.first.equals(address)) {
+                            peerAddress = addr.first;
+                            peerIdentityAddress = addr.second;
                             break;
                         }
                     }
@@ -2420,8 +2459,9 @@
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
             mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
-                    new DeviceInfo(device, name, address, codec,
-                            peerAddress, groupId));
+                    new DeviceInfo(device, name, address,
+                            btInfo.mDevice.getIdentityAddress(), codec,
+                            groupId, peerAddress, peerIdentityAddress));
             mDeviceBroker.postAccessoryPlugMediaUnmute(device);
             setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
             addAudioDeviceInInventoryIfNeeded(device, address, peerAddress,
@@ -2806,18 +2846,19 @@
         mDevRoleCapturePresetDispatchers.finishBroadcast();
     }
 
-    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+    List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) {
         List<String> addresses = new ArrayList<String>();
         final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(),
                 device.getAddress());
         synchronized (mDevicesLock) {
             DeviceInfo di = mConnectedDevices.get(key);
             if (di != null) {
-                if (!di.mDeviceAddress.isEmpty()) {
-                    addresses.add(di.mDeviceAddress);
+                if (!di.mDeviceIdentityAddress.isEmpty()) {
+                    addresses.add(di.mDeviceIdentityAddress);
                 }
-                if (!di.mPeerDeviceAddress.isEmpty()) {
-                    addresses.add(di.mPeerDeviceAddress);
+                if (!di.mPeerIdentityDeviceAddress.isEmpty()
+                        && !di.mPeerIdentityDeviceAddress.equals(di.mDeviceIdentityAddress)) {
+                    addresses.add(di.mPeerIdentityDeviceAddress);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0c78231..37fe389 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -33,6 +33,7 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
 import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.INVALID_UID;
 import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
@@ -43,6 +44,7 @@
 import static com.android.media.audio.Flags.alarmMinVolumeZero;
 import static com.android.media.audio.Flags.bluetoothMacAddressAnonymization;
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
+import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
 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;
@@ -116,6 +118,7 @@
 import android.media.AudioSystem;
 import android.media.AudioTrack;
 import android.media.BluetoothProfileConnectionInfo;
+import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
 import android.media.IAudioModeDispatcher;
@@ -605,6 +608,7 @@
     };
 
     private final boolean mUseFixedVolume;
+    private final boolean mRingerModeAffectsAlarm;
     private final boolean mUseVolumeGroupAliases;
 
     // If absolute volume is supported in AVRCP device
@@ -1298,6 +1302,9 @@
         mUseFixedVolume = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_useFixedVolume);
 
+        mRingerModeAffectsAlarm = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_audio_ringer_mode_affects_alarm_stream);
+
         mRecordMonitor = new RecordingActivityMonitor(mContext);
         mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true);
 
@@ -4513,6 +4520,8 @@
                 + bluetoothMacAddressAnonymization());
         pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
                 + disablePrescaleAbsoluteVolume());
+        pw.println("\tandroid.media.audiopolicy.enableFadeManagerConfiguration:"
+                + enableFadeManagerConfiguration());
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -7015,6 +7024,19 @@
             ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_DTMF);
         }
 
+        if (ringerModeAffectsAlarm()) {
+            if (mRingerModeAffectsAlarm) {
+                boolean muteAlarmWithRinger =
+                        mSettings.getGlobalInt(mContentResolver,
+                        Settings.Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE,
+                        /* def= */ 0) != 0;
+                if (muteAlarmWithRinger) {
+                    ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_ALARM);
+                } else {
+                    ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_ALARM);
+                }
+            }
+        }
         if (ringerModeAffectedStreams != mRingerModeAffectedStreams) {
             mSettings.putSystemIntForUser(mContentResolver,
                     Settings.System.MODE_RINGER_STREAMS_AFFECTED,
@@ -9674,6 +9696,8 @@
                     Settings.Global.ZEN_MODE), false, this);
             mContentResolver.registerContentObserver(Settings.Global.getUriFor(
                     Settings.Global.ZEN_MODE_CONFIG_ETAG), false, this);
+            mContentResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.MUTE_ALARM_STREAM_WITH_RINGER_MODE), false, this);
             mContentResolver.registerContentObserver(Settings.System.getUriFor(
                 Settings.System.MODE_RINGER_STREAMS_AFFECTED), false, this);
             mContentResolver.registerContentObserver(Settings.Global.getUriFor(
@@ -12614,6 +12638,47 @@
     }
 
     /**
+     * see {@link AudioPolicy#setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration)}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int setFadeManagerConfigurationForFocusLoss(
+            @NonNull FadeManagerConfiguration fmcForFocusLoss) {
+        super.setFadeManagerConfigurationForFocusLoss_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+        Objects.requireNonNull(fmcForFocusLoss,
+                "Fade manager config for focus loss cannot be null");
+        validateFadeManagerConfiguration(fmcForFocusLoss);
+
+        return mPlaybackMonitor.setFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS,
+                fmcForFocusLoss);
+    }
+
+    /**
+     * see {@link AudioPolicy#clearFadeManagerConfigurationForFocusLoss()}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int clearFadeManagerConfigurationForFocusLoss() {
+        super.clearFadeManagerConfigurationForFocusLoss_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+
+        return mPlaybackMonitor.clearFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS);
+    }
+
+    /**
+     * see {@link AudioPolicy#getFadeManagerConfigurationForFocusLoss()}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() {
+        super.getFadeManagerConfigurationForFocusLoss_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+
+        return mPlaybackMonitor.getFadeManagerConfiguration(AudioManager.AUDIOFOCUS_LOSS);
+    }
+
+    /**
      * @see AudioManager#getHalVersion
      */
     public @Nullable AudioHalVersionInfo getHalVersion() {
@@ -12814,6 +12879,19 @@
         }
     }
 
+    private void ensureFadeManagerConfigIsEnabled() {
+        Preconditions.checkState(enableFadeManagerConfiguration(),
+                "Fade manager configuration not supported");
+    }
+
+    private void validateFadeManagerConfiguration(FadeManagerConfiguration fmc) {
+        // validate permission of audio attributes
+        List<AudioAttributes> attrs = fmc.getAudioAttributesWithVolumeShaperConfigs();
+        for (int index = 0; index < attrs.size(); index++) {
+            validateAudioAttributesUsage(attrs.get(index));
+        }
+    }
+
     //======================
     // Audio policy callbacks from AudioSystem for dynamic policies
     //======================
@@ -13114,6 +13192,7 @@
                             + "could not link to " + projection + " binder death", e);
                 }
             }
+
             int status = connectMixes();
             if (status != AudioSystem.SUCCESS) {
                 release();
@@ -13471,6 +13550,43 @@
         }
     }
 
+    /**
+     * see {@link AudioManager#dispatchAudioFocusChangeWithFade(AudioFocusInfo, int, AudioPolicy,
+     * List, FadeManagerConfiguration)}
+     */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int dispatchFocusChangeWithFade(AudioFocusInfo afi, int focusChange,
+            IAudioPolicyCallback pcb, List<AudioFocusInfo> otherActiveAfis,
+            FadeManagerConfiguration transientFadeMgrConfig) {
+        super.dispatchFocusChangeWithFade_enforcePermission();
+        ensureFadeManagerConfigIsEnabled();
+        Objects.requireNonNull(afi, "AudioFocusInfo cannot be null");
+        Objects.requireNonNull(pcb, "AudioPolicy callback cannot be null");
+        Objects.requireNonNull(otherActiveAfis,
+                "Other active AudioFocusInfo list cannot be null");
+        if (transientFadeMgrConfig != null) {
+            validateFadeManagerConfiguration(transientFadeMgrConfig);
+        }
+
+        synchronized (mAudioPolicies) {
+            Preconditions.checkState(mAudioPolicies.containsKey(pcb.asBinder()),
+                    "Unregistered AudioPolicy for focus dispatch with fade");
+
+            // set the transient fade manager config to be used for handling this focus change
+            if (transientFadeMgrConfig != null) {
+                mPlaybackMonitor.setTransientFadeManagerConfiguration(focusChange,
+                        transientFadeMgrConfig);
+            }
+            int status = mMediaFocusControl.dispatchFocusChangeWithFade(afi, focusChange,
+                    otherActiveAfis);
+
+            if (transientFadeMgrConfig != null) {
+                mPlaybackMonitor.clearTransientFadeManagerConfiguration(focusChange);
+            }
+            return status;
+        }
+    }
 
     //======================
     // Audioserver state dispatch
@@ -13775,8 +13891,8 @@
         return activeAssistantUids;
     }
 
-    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
-        return mDeviceBroker.getDeviceAddresses(device);
+    List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) {
+        return mDeviceBroker.getDeviceIdentityAddresses(device);
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 8075618..a818c30 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -58,6 +58,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.utils.EventLogger;
@@ -1077,8 +1078,8 @@
         return mLeAudio.getGroupId(device);
     }
 
-    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
-        List<String> addresses = new ArrayList<String>();
+    /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) {
+        List<Pair<String, String>> addresses = new ArrayList<>();
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         if (adapter == null || mLeAudio == null) {
             return addresses;
@@ -1086,7 +1087,7 @@
         List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
         for (BluetoothDevice device : activeDevices) {
             if (device != null && mLeAudio.getGroupId(device) == groupId) {
-                addresses.add(device.getAddress());
+                addresses.add(new Pair(device.getAddress(), device.getIdentityAddress()));
             }
         }
         return addresses;
diff --git a/services/core/java/com/android/server/audio/FadeConfigurations.java b/services/core/java/com/android/server/audio/FadeConfigurations.java
index 2e27c76..37ecf0b 100644
--- a/services/core/java/com/android/server/audio/FadeConfigurations.java
+++ b/services/core/java/com/android/server/audio/FadeConfigurations.java
@@ -16,13 +16,22 @@
 
 package com.android.server.audio;
 
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.media.AudioAttributes;
+import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
+import android.media.FadeManagerConfiguration;
 import android.media.VolumeShaper;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Class to encapsulate configurations used for fading players
@@ -69,51 +78,229 @@
 
     private static final int INVALID_UID = -1;
 
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mDefaultFadeManagerConfig;
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mUpdatedFadeManagerConfig;
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mTransientFadeManagerConfig;
+    /** active fade manager is one of: transient > updated > default */
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration mActiveFadeManagerConfig;
+
+    /**
+     * Sets the custom fade manager configuration
+     *
+     * @param fadeManagerConfig custom fade manager configuration
+     * @return {@link AudioManager#SUCCESS}  if setting custom fade manager configuration succeeds
+     *     or {@link AudioManager#ERROR} otherwise (example - when fade manager configuration
+     *     feature is disabled)
+     */
+    public int setFadeManagerConfiguration(
+            @NonNull FadeManagerConfiguration fadeManagerConfig) {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+
+        synchronized (mLock) {
+            mUpdatedFadeManagerConfig = Objects.requireNonNull(fadeManagerConfig,
+                    "Fade manager configuration cannot be null");
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Clears the fade manager configuration that was previously set with
+     * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS}  if previously set fade manager configuration is cleared
+     *     or {@link AudioManager#ERROR} otherwise (example, when fade manager configuration feature
+     *     is disabled)
+     */
+    public int clearFadeManagerConfiguration() {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+
+        synchronized (mLock) {
+            mUpdatedFadeManagerConfig = null;
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Returns the active fade manager configuration
+     *
+     * @return {@code null} if feature is disabled, or the custom fade manager configuration if set,
+     *     or default fade manager configuration if not set.
+     */
+    @Nullable
+    public FadeManagerConfiguration getFadeManagerConfiguration() {
+        if (!enableFadeManagerConfiguration()) {
+            return null;
+        }
+
+        synchronized (mLock) {
+            return mActiveFadeManagerConfig;
+        }
+    }
+
+    /**
+     * Sets the transient fade manager configuration
+     *
+     * @param fadeManagerConfig custom fade manager configuration
+     * @return {@link AudioManager#SUCCESS} if setting custom fade manager configuration succeeds
+     *     or {@link AudioManager#ERROR} otherwise (example - when fade manager configuration is
+     *     disabled)
+     */
+    public int setTransientFadeManagerConfiguration(
+            @NonNull FadeManagerConfiguration fadeManagerConfig) {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+
+        synchronized (mLock) {
+            mTransientFadeManagerConfig = Objects.requireNonNull(fadeManagerConfig,
+                    "Transient FadeManagerConfiguration cannot be null");
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Clears the transient fade manager configuration that was previously set with
+     * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS} if previously set transient fade manager configuration
+     *     is cleared or {@link AudioManager#ERROR} otherwise (example - when fade manager
+     *     configuration is disabled)
+     */
+    public int clearTransientFadeManagerConfiguration() {
+        if (!enableFadeManagerConfiguration()) {
+            return AudioManager.ERROR;
+        }
+        synchronized (mLock) {
+            mTransientFadeManagerConfig = null;
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return AudioManager.SUCCESS;
+    }
+
+    /**
+     * Query if fade should be enforecd on players
+     *
+     * @return {@code true} if fade is enabled or using default configurations, {@code false}
+     * otherwise.
+     */
+    public boolean isFadeEnabled() {
+        if (!enableFadeManagerConfiguration()) {
+            return true;
+        }
+
+        synchronized (mLock) {
+            return getUpdatedFadeManagerConfigLocked().isFadeEnabled();
+        }
+    }
+
     /**
      * Query {@link android.media.AudioAttributes.AttributeUsage usages} that are allowed to
      * fade
+     *
      * @return list of {@link android.media.AudioAttributes.AttributeUsage}
      */
     @NonNull
     public List<Integer> getFadeableUsages() {
-        return DEFAULT_FADEABLE_USAGES;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADEABLE_USAGES;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return an empty list instead
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getFadeableUsages()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Query {@link android.media.AudioAttributes.AttributeContentType content types} that are
      * exempted from fade enforcement
+     *
      * @return list of {@link android.media.AudioAttributes.AttributeContentType}
      */
     @NonNull
     public List<Integer> getUnfadeableContentTypes() {
-        return DEFAULT_UNFADEABLE_CONTENT_TYPES;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_CONTENT_TYPES;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return an empty list instead
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeableContentTypes()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Query {@link android.media.AudioPlaybackConfiguration.PlayerType player types} that are
      * exempted from fade enforcement
+     *
      * @return list of {@link android.media.AudioPlaybackConfiguration.PlayerType}
      */
     @NonNull
     public List<Integer> getUnfadeablePlayerTypes() {
-        return DEFAULT_UNFADEABLE_PLAYER_TYPES;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_PLAYER_TYPES;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return an empty list instead
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeablePlayerTypes()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Get the {@link android.media.VolumeShaper.Configuration} configuration to be applied
      * for the fade-out
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @return {@link android.media.VolumeShaper.Configuration} for the
-     * {@link android.media.AudioAttributes.AttributeUsage} or default volume shaper if not
-     * configured
+     * {@link android.media.AudioAttributes} or default volume shaper if not configured
      */
     @NonNull
     public VolumeShaper.Configuration getFadeOutVolumeShaperConfig(@NonNull AudioAttributes aa) {
-        return DEFAULT_FADEOUT_VSHAPE;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADEOUT_VSHAPE;
+        }
+        return getOptimalFadeOutVolShaperConfig(aa);
     }
 
     /**
+     * Get the {@link android.media.VolumeShaper.Configuration} configuration to be applied for the
+     * fade in
+     *
+     * @param aa The {@link android.media.AudioAttributes}
+     * @return {@link android.media.VolumeShaper.Configuration} for the
+     * {@link android.media.AudioAttributes} or {@code null} otherwise
+     */
+    @Nullable
+    public VolumeShaper.Configuration getFadeInVolumeShaperConfig(@NonNull AudioAttributes aa) {
+        if (!enableFadeManagerConfiguration()) {
+            return null;
+        }
+        return getOptimalFadeInVolShaperConfig(aa);
+    }
+
+
+    /**
      * Get the duration to fade out a player of type usage
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @return duration in milliseconds for the
      * {@link android.media.AudioAttributes} or default duration if not configured
@@ -122,22 +309,73 @@
         if (!isFadeable(aa, INVALID_UID, AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN)) {
             return 0;
         }
-        return DEFAULT_FADE_OUT_DURATION_MS;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADE_OUT_DURATION_MS;
+        }
+        return getOptimalFadeOutDuration(aa);
     }
 
     /**
-     * Get the delay to fade in offending players that do not stop after losing audio focus.
+     * Get the delay to fade in offending players that do not stop after losing audio focus
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @return delay in milliseconds for the
      * {@link android.media.AudioAttributes.Attribute} or default delay if not configured
      */
     public long getDelayFadeInOffenders(@NonNull AudioAttributes aa) {
-        return DEFAULT_DELAY_FADE_IN_OFFENDERS_MS;
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_DELAY_FADE_IN_OFFENDERS_MS;
+        }
+
+        synchronized (mLock) {
+            return getUpdatedFadeManagerConfigLocked().getFadeInDelayForOffenders();
+        }
+    }
+
+    /**
+     * Query {@link android.media.AudioAttributes} that are exempted from fade enforcement
+     *
+     * @return list of {@link android.media.AudioAttributes}
+     */
+    @NonNull
+    public List<AudioAttributes> getUnfadeableAudioAttributes() {
+        // unfadeable audio attributes is only supported with fade manager configurations
+        if (!enableFadeManagerConfiguration()) {
+            return Collections.EMPTY_LIST;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return empty list
+            return fadeManagerConfig.isFadeEnabled()
+                    ? fadeManagerConfig.getUnfadeableAudioAttributes() : Collections.EMPTY_LIST;
+        }
+    }
+
+    /**
+     * Query uids that are exempted from fade enforcement
+     *
+     * @return list of uids
+     */
+    @NonNull
+    public List<Integer> getUnfadeableUids() {
+        // unfadeable uids is only supported with fade manager configurations
+        if (!enableFadeManagerConfiguration()) {
+            return Collections.EMPTY_LIST;
+        }
+
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // when fade is not enabled, return empty list
+            return fadeManagerConfig.isFadeEnabled() ? fadeManagerConfig.getUnfadeableUids()
+                    : Collections.EMPTY_LIST;
+        }
     }
 
     /**
      * Check if it is allowed to fade for the given {@link android.media.AudioAttributes},
-     * client uid and {@link android.media.AudioPlaybackConfiguration.PlayerType} config.
+     * client uid and {@link android.media.AudioPlaybackConfiguration.PlayerType} config
+     *
      * @param aa The {@link android.media.AudioAttributes}
      * @param uid The uid of the client owning the player
      * @param playerType The {@link android.media.AudioPlaybackConfiguration.PlayerType}
@@ -145,36 +383,173 @@
      */
     public boolean isFadeable(@NonNull AudioAttributes aa, int uid,
             @AudioPlaybackConfiguration.PlayerType int playerType) {
-        if (isPlayerTypeUnfadeable(playerType)) {
-            if (DEBUG) {
-                Slog.i(TAG, "not fadeable: player type:" + playerType);
+        synchronized (mLock) {
+            if (isPlayerTypeUnfadeableLocked(playerType)) {
+                if (DEBUG) {
+                    Slog.i(TAG, "not fadeable: player type:" + playerType);
+                }
+                return false;
             }
-            return false;
-        }
-        if (isContentTypeUnfadeable(aa.getContentType())) {
-            if (DEBUG) {
-                Slog.i(TAG, "not fadeable: content type:" + aa.getContentType());
+            if (isContentTypeUnfadeableLocked(aa.getContentType())) {
+                if (DEBUG) {
+                    Slog.i(TAG, "not fadeable: content type:" + aa.getContentType());
+                }
+                return false;
             }
-            return false;
-        }
-        if (!isUsageFadeable(aa.getUsage())) {
-            if (DEBUG) {
-                Slog.i(TAG, "not fadeable: usage:" + aa.getUsage());
+            if (!isUsageFadeableLocked(aa.getSystemUsage())) {
+                if (DEBUG) {
+                    Slog.i(TAG, "not fadeable: usage:" + aa.getUsage());
+                }
+                return false;
             }
-            return false;
+            // new configs using fade manager configuration
+            if (isUnfadeableForFadeMgrConfigLocked(aa, uid)) {
+                return false;
+            }
+            return true;
         }
-        return true;
     }
 
-    private boolean isUsageFadeable(int usage) {
-        return getFadeableUsages().contains(usage);
+    /** Tries to get the fade out volume shaper config closest to the audio attributes */
+    private VolumeShaper.Configuration getOptimalFadeOutVolShaperConfig(AudioAttributes aa) {
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // check if the specific audio attributes has a volume shaper config defined
+            VolumeShaper.Configuration volShaperConfig =
+                    fadeManagerConfig.getFadeOutVolumeShaperConfigForAudioAttributes(aa);
+            if (volShaperConfig != null) {
+                return volShaperConfig;
+            }
+
+            // get the volume shaper config for usage
+            // for fadeable usages, this should never return null
+            return fadeManagerConfig.getFadeOutVolumeShaperConfigForUsage(
+                    aa.getSystemUsage());
+        }
     }
 
-    private boolean isContentTypeUnfadeable(int contentType) {
-        return getUnfadeableContentTypes().contains(contentType);
+    /** Tries to get the fade in volume shaper config closest to the audio attributes */
+    private VolumeShaper.Configuration getOptimalFadeInVolShaperConfig(AudioAttributes aa) {
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // check if the specific audio attributes has a volume shaper config defined
+            VolumeShaper.Configuration volShaperConfig =
+                    fadeManagerConfig.getFadeInVolumeShaperConfigForAudioAttributes(aa);
+            if (volShaperConfig != null) {
+                return volShaperConfig;
+            }
+
+            // get the volume shaper config for usage
+            // for fadeable usages, this should never return null
+            return fadeManagerConfig.getFadeInVolumeShaperConfigForUsage(aa.getSystemUsage());
+        }
     }
 
-    private boolean isPlayerTypeUnfadeable(int playerType) {
-        return getUnfadeablePlayerTypes().contains(playerType);
+    /** Tries to get the duration closest to the audio attributes */
+    private long getOptimalFadeOutDuration(AudioAttributes aa) {
+        synchronized (mLock) {
+            FadeManagerConfiguration fadeManagerConfig = getUpdatedFadeManagerConfigLocked();
+            // check if specific audio attributes has a duration defined
+            long duration = fadeManagerConfig.getFadeOutDurationForAudioAttributes(aa);
+            if (duration != FadeManagerConfiguration.DURATION_NOT_SET) {
+                return duration;
+            }
+
+            // get the duration for usage
+            // for fadeable usages, this should never return DURATION_NOT_SET
+            return fadeManagerConfig.getFadeOutDurationForUsage(aa.getSystemUsage());
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isUnfadeableForFadeMgrConfigLocked(AudioAttributes aa, int uid) {
+        if (isAudioAttributesUnfadeableLocked(aa)) {
+            if (DEBUG) {
+                Slog.i(TAG, "not fadeable: aa:" + aa);
+            }
+            return true;
+        }
+        if (isUidUnfadeableLocked(uid)) {
+            if (DEBUG) {
+                Slog.i(TAG, "not fadeable: uid:" + uid);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @GuardedBy("mLock")
+    private boolean isUsageFadeableLocked(int usage) {
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_FADEABLE_USAGES.contains(usage);
+        }
+        return getUpdatedFadeManagerConfigLocked().isUsageFadeable(usage);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isContentTypeUnfadeableLocked(int contentType) {
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_CONTENT_TYPES.contains(contentType);
+        }
+        return getUpdatedFadeManagerConfigLocked().isContentTypeUnfadeable(contentType);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isPlayerTypeUnfadeableLocked(int playerType) {
+        if (!enableFadeManagerConfiguration()) {
+            return DEFAULT_UNFADEABLE_PLAYER_TYPES.contains(playerType);
+        }
+        return getUpdatedFadeManagerConfigLocked().isPlayerTypeUnfadeable(playerType);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isAudioAttributesUnfadeableLocked(AudioAttributes aa) {
+        if (!enableFadeManagerConfiguration()) {
+            // default fade configs do not support unfadeable audio attributes, hence return false
+            return false;
+        }
+        return getUpdatedFadeManagerConfigLocked().isAudioAttributesUnfadeable(aa);
+    }
+
+    @GuardedBy("mLock")
+    private boolean isUidUnfadeableLocked(int uid) {
+        if (!enableFadeManagerConfiguration()) {
+            // default fade configs do not support unfadeable uids, hence return false
+            return false;
+        }
+        return getUpdatedFadeManagerConfigLocked().isUidUnfadeable(uid);
+    }
+
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration getUpdatedFadeManagerConfigLocked() {
+        if (mActiveFadeManagerConfig == null) {
+            mActiveFadeManagerConfig = getActiveFadeMgrConfigLocked();
+        }
+        return mActiveFadeManagerConfig;
+    }
+
+    /** Priority between fade manager configs: Transient > Updated > Default */
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration getActiveFadeMgrConfigLocked() {
+        // below configs are arranged in the order of priority
+        // configs placed higher have higher priority
+        if (mTransientFadeManagerConfig != null) {
+            return mTransientFadeManagerConfig;
+        }
+
+        if (mUpdatedFadeManagerConfig != null) {
+            return mUpdatedFadeManagerConfig;
+        }
+
+        // default - must be the lowest priority
+        return getDefaultFadeManagerConfigLocked();
+    }
+
+    @GuardedBy("mLock")
+    private FadeManagerConfiguration getDefaultFadeManagerConfigLocked() {
+        if (mDefaultFadeManagerConfig == null) {
+            mDefaultFadeManagerConfig = new FadeManagerConfiguration.Builder().build();
+        }
+        return mDefaultFadeManagerConfig;
     }
 }
diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java
index 1171f97..cbcd8f5 100644
--- a/services/core/java/com/android/server/audio/FadeOutManager.java
+++ b/services/core/java/com/android/server/audio/FadeOutManager.java
@@ -16,21 +16,24 @@
 
 package com.android.server.audio;
 
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
+import android.media.FadeManagerConfiguration;
 import android.media.VolumeShaper;
 import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Objects;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Class to handle fading out players
@@ -40,14 +43,6 @@
     public static final String TAG = "AS.FadeOutManager";
 
     private static final boolean DEBUG = PlaybackActivityMonitor.DEBUG;
-    private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
-            new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
-                    .createIfNeeded()
-                    .build();
-
-    // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
-    private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
-            new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
 
     private final Object mLock = new Object();
 
@@ -57,16 +52,81 @@
     @GuardedBy("mLock")
     private final SparseArray<FadedOutApp> mUidToFadedAppsMap = new SparseArray<>();
 
-    @GuardedBy("mLock")
-    private FadeConfigurations mFadeConfigurations;
+    private final FadeConfigurations mFadeConfigurations = new FadeConfigurations();
 
-    public FadeOutManager() {
-        mFadeConfigurations = new FadeConfigurations();
+    /**
+     * Sets the custom fade manager configuration to be used for player fade out and in
+     *
+     * @param fadeManagerConfig custom fade manager configuration
+     * @return {@link AudioManager#SUCCESS} if setting fade manager config succeeded,
+     *     {@link AudioManager#ERROR} otherwise
+     */
+    int setFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.setFadeManagerConfiguration(fadeManagerConfig);
+        }
     }
 
-    public FadeOutManager(FadeConfigurations fadeConfigurations) {
-        mFadeConfigurations = Objects.requireNonNull(fadeConfigurations,
-                "Fade configurations can not be null");
+    /**
+     * Clears the fade manager configuration that was previously set with
+     * {@link #setFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS}  if clearing fade manager config succeeded,
+     *     {@link AudioManager#ERROR} otherwise
+     */
+    int clearFadeManagerConfiguration() {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.clearFadeManagerConfiguration();
+        }
+    }
+
+    /**
+     * Returns the active fade manager configuration
+     *
+     * @return the {@link FadeManagerConfiguration}
+     */
+    FadeManagerConfiguration getFadeManagerConfiguration() {
+        return mFadeConfigurations.getFadeManagerConfiguration();
+    }
+
+    /**
+     * Sets the transient fade manager configuration to be used for player fade out and in
+     *
+     * @param fadeManagerConfig fade manager config that has higher priority than the existing
+     *     fade manager configuration. This is expected to be transient.
+     * @return {@link AudioManager#SUCCESS}  if setting fade manager config succeeded,
+     *     {@link AudioManager#ERROR} otherwise
+     */
+    int setTransientFadeManagerConfiguration(FadeManagerConfiguration fadeManagerConfig) {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.setTransientFadeManagerConfiguration(fadeManagerConfig);
+        }
+    }
+
+    /**
+     * Clears the transient fade manager configuration that was previously set with
+     * {@link #setTransientFadeManagerConfiguration(FadeManagerConfiguration)}
+     *
+     * @return {@link AudioManager#SUCCESS}  if clearing fade manager config succeeded,
+     *      {@link AudioManager#ERROR} otherwise
+     */
+    int clearTransientFadeManagerConfiguration() {
+        // locked to ensure the fade configs are not updated while faded app state is being updated
+        synchronized (mLock) {
+            return mFadeConfigurations.clearTransientFadeManagerConfiguration();
+        }
+    }
+
+    /**
+     * Query if fade is enblead and can be enforced on players
+     *
+     * @return {@code true} if fade is enabled, {@code false} otherwise.
+     */
+    boolean isFadeEnabled() {
+        return mFadeConfigurations.isFadeEnabled();
     }
 
     // TODO explore whether a shorter fade out would be a better UX instead of not fading out at all
@@ -128,7 +188,7 @@
         }
     }
 
-    void fadeOutUid(int uid, ArrayList<AudioPlaybackConfiguration> players) {
+    void fadeOutUid(int uid, List<AudioPlaybackConfiguration> players) {
         Slog.i(TAG, "fadeOutUid() uid:" + uid);
         synchronized (mLock) {
             if (!mUidToFadedAppsMap.contains(uid)) {
@@ -148,15 +208,31 @@
      * @param uid the uid for the app to unfade out
      * @param players map of current available players (so we can get an APC from piid)
      */
-    void unfadeOutUid(int uid, HashMap<Integer, AudioPlaybackConfiguration> players) {
+    void unfadeOutUid(int uid, Map<Integer, AudioPlaybackConfiguration> players) {
         Slog.i(TAG, "unfadeOutUid() uid:" + uid);
         synchronized (mLock) {
-            final FadedOutApp fa = mUidToFadedAppsMap.get(uid);
+            FadedOutApp fa = mUidToFadedAppsMap.get(uid);
             if (fa == null) {
                 return;
             }
             mUidToFadedAppsMap.remove(uid);
-            fa.removeUnfadeAll(players);
+
+            if (!enableFadeManagerConfiguration()) {
+                fa.removeUnfadeAll(players);
+                return;
+            }
+
+            // since fade manager configs may have volume-shaper config per audio attributes,
+            // iterate through each palyer and gather respective configs  for fade in
+            ArrayList<AudioPlaybackConfiguration> apcs = new ArrayList<>(players.values());
+            for (int index = 0; index < apcs.size(); index++) {
+                AudioPlaybackConfiguration apc = apcs.get(index);
+                VolumeShaper.Configuration config = mFadeConfigurations
+                        .getFadeInVolumeShaperConfig(apc.getAudioAttributes());
+                fa.fadeInPlayer(apc, config);
+            }
+            // ideal case all players should be faded in
+            fa.clear();
         }
     }
 
@@ -209,16 +285,6 @@
         }
     }
 
-    /**
-     * Update fade configurations used for player fade operations
-     * @param fadeConfigurations set of configs that define fade properties
-     */
-    void setFadeConfigurations(@NonNull FadeConfigurations fadeConfigurations) {
-        synchronized (mLock) {
-            mFadeConfigurations = fadeConfigurations;
-        }
-    }
-
     void dump(PrintWriter pw) {
         synchronized (mLock) {
             for (int index = 0; index < mUidToFadedAppsMap.size(); index++) {
@@ -232,6 +298,15 @@
      * Class to group players from a common app, that are faded out.
      */
     private static final class FadedOutApp {
+        private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
+                new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
+                        .createIfNeeded()
+                        .build();
+
+        // like a PLAY_CREATE_IF_NEEDED operation but with a skip to the end of the ramp
+        private static final VolumeShaper.Operation PLAY_SKIP_RAMP =
+                new VolumeShaper.Operation.Builder(PLAY_CREATE_IF_NEEDED).setXOffset(1.0f).build();
+
         private final int mUid;
         // key -> piid; value -> volume shaper config applied
         private final SparseArray<VolumeShaper.Configuration> mFadedPlayers = new SparseArray<>();
@@ -269,17 +344,9 @@
                 return;
             }
             if (apc.getPlayerProxy() != null) {
-                try {
-                    PlaybackActivityMonitor.sEventLogger.enqueue(
-                            (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog(
-                                    TAG));
-                    apc.getPlayerProxy().applyVolumeShaper(volShaper,
-                            skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
-                    mFadedPlayers.put(piid, volShaper);
-                } catch (Exception e) {
-                    Slog.e(TAG, "Error fading out player piid:" + piid
-                            + " uid:" + apc.getClientUid(), e);
-                }
+                applyVolumeShaperInternal(apc, piid, volShaper,
+                        skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
+                mFadedPlayers.put(piid, volShaper);
             } else {
                 if (DEBUG) {
                     Slog.v(TAG, "Error fading out player piid:" + piid
@@ -288,21 +355,13 @@
             }
         }
 
-        void removeUnfadeAll(HashMap<Integer, AudioPlaybackConfiguration> players) {
+        void removeUnfadeAll(Map<Integer, AudioPlaybackConfiguration> players) {
             for (int index = 0; index < mFadedPlayers.size(); index++) {
                 int piid = mFadedPlayers.keyAt(index);
                 final AudioPlaybackConfiguration apc = players.get(piid);
                 if ((apc != null) && (apc.getPlayerProxy() != null)) {
-                    final VolumeShaper.Configuration volShaper = mFadedPlayers.valueAt(index);
-                    try {
-                        PlaybackActivityMonitor.sEventLogger.enqueue(
-                                (new EventLogger.StringEvent("unfading out piid:"
-                                        + piid)).printLog(TAG));
-                        apc.getPlayerProxy().applyVolumeShaper(volShaper,
-                                VolumeShaper.Operation.REVERSE);
-                    } catch (Exception e) {
-                        Slog.e(TAG, "Error unfading out player piid:" + piid + " uid:" + mUid, e);
-                    }
+                    applyVolumeShaperInternal(apc, piid, /* volShaperConfig= */ null,
+                            VolumeShaper.Operation.REVERSE);
                 } else {
                     // this piid was in the list of faded players, but wasn't found
                     if (DEBUG) {
@@ -314,8 +373,61 @@
             mFadedPlayers.clear();
         }
 
+        void fadeInPlayer(@NonNull AudioPlaybackConfiguration apc,
+                @Nullable VolumeShaper.Configuration config) {
+            int piid = Integer.valueOf(apc.getPlayerInterfaceId());
+            // if not found, no need to fade in since it was never faded out
+            if (!mFadedPlayers.contains(piid)) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Player (piid: " + piid + ") for uid (" + mUid
+                            + ") is not faded out, no need to fade in");
+                }
+                return;
+            }
+
+            mFadedPlayers.remove(piid);
+            if (apc.getPlayerProxy() != null) {
+                applyVolumeShaperInternal(apc, piid, config,
+                        config != null ? PLAY_CREATE_IF_NEEDED : VolumeShaper.Operation.REVERSE);
+            } else {
+                if (DEBUG) {
+                    Slog.v(TAG, "Error fading in player piid:" + piid
+                            + ", player not found for uid " + mUid);
+                }
+            }
+        }
+
+        void clear() {
+            if (mFadedPlayers.size() > 0) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Non empty faded players list being cleared! Faded out players:"
+                            + mFadedPlayers);
+                }
+            }
+            // should the players be faded in irrespective?
+            mFadedPlayers.clear();
+        }
+
         void removeReleased(@NonNull AudioPlaybackConfiguration apc) {
             mFadedPlayers.delete(Integer.valueOf(apc.getPlayerInterfaceId()));
         }
+
+        private void applyVolumeShaperInternal(AudioPlaybackConfiguration apc, int piid,
+                VolumeShaper.Configuration volShaperConfig, VolumeShaper.Operation operation) {
+            VolumeShaper.Configuration config = volShaperConfig;
+            // when operation is reverse, use the fade out volume shaper config instead
+            if (operation.equals(VolumeShaper.Operation.REVERSE)) {
+                config = mFadedPlayers.get(piid);
+            }
+            try {
+                PlaybackActivityMonitor.sEventLogger.enqueue(
+                        (new PlaybackActivityMonitor.FadeEvent(apc, config, operation))
+                                .printLog(TAG));
+                apc.getPlayerProxy().applyVolumeShaper(config, operation);
+            } catch (Exception e) {
+                Slog.e(TAG, "Error fading player piid:" + piid + " uid:" + mUid
+                        + " operation:" + operation, e);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java
index 00c04ff..f462539 100644
--- a/services/core/java/com/android/server/audio/FocusRequester.java
+++ b/services/core/java/com/android/server/audio/FocusRequester.java
@@ -33,6 +33,7 @@
 import com.android.server.pm.UserManagerInternal;
 
 import java.io.PrintWriter;
+import java.util.List;
 
 /**
  * @hide
@@ -534,6 +535,33 @@
         return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
     }
 
+    @GuardedBy("MediaFocusControl.mAudioFocusLock")
+    int dispatchFocusChangeWithFadeLocked(int focusChange, List<FocusRequester> otherActiveFrs) {
+        if (focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                || focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+            mFocusLossFadeLimbo = false;
+            mFocusController.restoreVShapedPlayers(this);
+        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS
+                && mFocusController.shouldEnforceFade()) {
+            for (int index = 0; index < otherActiveFrs.size(); index++) {
+                // candidate for fade-out before a receiving a loss
+                if (mFocusController.fadeOutPlayers(otherActiveFrs.get(index), /* loser= */ this)) {
+                    // active players are being faded out, delay the dispatch of focus loss
+                    // mark this instance as being faded so it's not released yet as the focus loss
+                    // will be dispatched later, it is now in limbo mode
+                    mFocusLossFadeLimbo = true;
+                    mFocusController.postDelayedLossAfterFade(this,
+                            mFocusController.getFadeOutDurationOnFocusLossMillis(
+                                    this.getAudioAttributes()));
+                    return AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
+                }
+            }
+        }
+        return dispatchFocusChange(focusChange);
+    }
+
     void dispatchFocusResultFromExtPolicy(int requestResult) {
         final IAudioFocusDispatcher fd = mFocusDispatcher;
         if (fd == null) {
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 58f5d5e..0df0006 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.audio;
 
+import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
@@ -195,6 +197,15 @@
         }
         return mFocusEnforcer.getFadeInDelayForOffendersMillis(aa);
     }
+
+    @Override
+    public boolean shouldEnforceFade() {
+        if (!enableFadeManagerConfiguration()) {
+            return ENFORCE_FADEOUT_FOR_FOCUS_LOSS;
+        }
+
+        return mFocusEnforcer.shouldEnforceFade();
+    }
     //==========================================================================================
     // AudioFocus
     //==========================================================================================
@@ -861,14 +872,17 @@
                 return;
             }
         }
-        final FocusRequester fr;
-        if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
-            fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
-        } else {
-            fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
-        }
-        if (fr != null) {
-            fr.dispatchFocusResultFromExtPolicy(requestResult);
+        synchronized (mAudioFocusLock) {
+            FocusRequester fr = getFocusRequesterLocked(afi.getClientId(),
+                    /* shouldRemove= */ requestResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED);
+            if (fr != null) {
+                fr.dispatchFocusResultFromExtPolicy(requestResult);
+                // if fade is enabled for external focus policies, apply it when setting
+                // focus result as well
+                if (enableFadeManagerConfiguration()) {
+                    fr.handleFocusGainFromRequest(requestResult);
+                }
+            }
         }
     }
 
@@ -902,24 +916,80 @@
                     + afi.getClientId());
         }
         synchronized (mAudioFocusLock) {
-            if (mFocusPolicy == null) {
-                if (DEBUG) { Log.v(TAG, "> failed: no focus policy" ); }
-                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
-            }
-            final FocusRequester fr;
-            if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
-                fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId());
-            } else {
-                fr = mFocusOwnersForFocusPolicy.get(afi.getClientId());
-            }
+            FocusRequester fr = getFocusRequesterLocked(afi.getClientId(),
+                    /* shouldRemove= */ focusChange == AudioManager.AUDIOFOCUS_LOSS);
             if (fr == null) {
-                if (DEBUG) { Log.v(TAG, "> failed: no such focus requester known" ); }
+                if (DEBUG) {
+                    Log.v(TAG, "> failed: no such focus requester known");
+                }
                 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
             }
             return fr.dispatchFocusChange(focusChange);
         }
     }
 
+    int dispatchFocusChangeWithFade(AudioFocusInfo afi, int focusChange,
+            List<AudioFocusInfo> otherActiveAfis) {
+        if (DEBUG) {
+            Log.v(TAG, "dispatchFocusChangeWithFade " + AudioManager.audioFocusToString(focusChange)
+                    + " to afi client=" + afi.getClientId()
+                    + " other active afis=" + otherActiveAfis);
+        }
+
+        synchronized (mAudioFocusLock) {
+            String clientId = afi.getClientId();
+            // do not remove the entry since it can be posted for fade
+            FocusRequester fr = getFocusRequesterLocked(clientId, /* shouldRemove= */ false);
+            if (fr == null) {
+                if (DEBUG) {
+                    Log.v(TAG, "> failed: no such focus requester known");
+                }
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+
+            // convert other AudioFocusInfo to corresponding FocusRequester
+            ArrayList<FocusRequester> otherActiveFrs = new ArrayList<>();
+            for (int index = 0; index < otherActiveAfis.size(); index++) {
+                FocusRequester otherFr = getFocusRequesterLocked(
+                        otherActiveAfis.get(index).getClientId(), /* shouldRemove= */ false);
+                if (otherFr == null) {
+                    continue;
+                }
+                otherActiveFrs.add(otherFr);
+            }
+
+            int status = fr.dispatchFocusChangeWithFadeLocked(focusChange, otherActiveFrs);
+            if (status != AudioManager.AUDIOFOCUS_REQUEST_DELAYED
+                    && focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+                mFocusOwnersForFocusPolicy.remove(clientId);
+            }
+
+            return status;
+        }
+    }
+
+    @GuardedBy("mAudioFocusLock")
+    private FocusRequester getFocusRequesterLocked(String clientId, boolean shouldRemove) {
+        if (mFocusPolicy == null) {
+            if (DEBUG) {
+                Log.v(TAG, "> failed: no focus policy");
+            }
+            return null;
+        }
+
+        FocusRequester fr;
+        if (shouldRemove) {
+            fr = mFocusOwnersForFocusPolicy.remove(clientId);
+        } else {
+            fr = mFocusOwnersForFocusPolicy.get(clientId);
+        }
+
+        if (fr == null && DEBUG) {
+            Log.v(TAG, "> failed: no such focus requester known");
+        }
+        return fr;
+    }
+
     private void dumpExtFocusPolicyFocusOwners(PrintWriter pw) {
         final Set<Entry<String, FocusRequester>> owners = mFocusOwnersForFocusPolicy.entrySet();
         final Iterator<Entry<String, FocusRequester>> ownerIterator = owners.iterator();
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index bc9b9b4..e69fbbd 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -38,6 +38,7 @@
 import android.media.AudioPlaybackConfiguration.FormatInfo;
 import android.media.AudioPlaybackConfiguration.PlayerMuteEvent;
 import android.media.AudioSystem;
+import android.media.FadeManagerConfiguration;
 import android.media.IPlaybackConfigDispatcher;
 import android.media.PlayerBase;
 import android.media.VolumeShaper;
@@ -156,8 +157,7 @@
     private final int mMaxAlarmVolume;
     private int mPrivilegedAlarmActiveCount = 0;
     private final Consumer<AudioDeviceAttributes> mMuteAwaitConnectionTimeoutCb;
-    private final FadeOutManager mFadeOutManager;
-
+    private final FadeOutManager mFadeOutManager = new FadeOutManager();
 
     PlaybackActivityMonitor(Context context, int maxAlarmVolume,
             Consumer<AudioDeviceAttributes> muteTimeoutCallback) {
@@ -167,7 +167,6 @@
         AudioPlaybackConfiguration.sPlayerDeathMonitor = this;
         mMuteAwaitConnectionTimeoutCb = muteTimeoutCallback;
         initEventHandler();
-        mFadeOutManager = new FadeOutManager(new FadeConfigurations());
     }
 
     //=================================================================
@@ -971,6 +970,12 @@
         return mFadeOutManager.getFadeInDelayForOffendersMillis(aa);
     }
 
+    @Override
+    public boolean shouldEnforceFade() {
+        return mFadeOutManager.isFadeEnabled();
+    }
+
+
     //=================================================================
     // Track playback activity listeners
 
@@ -1010,6 +1015,27 @@
         }
     }
 
+    int setFadeManagerConfiguration(int focusType, FadeManagerConfiguration fadeMgrConfig) {
+        return mFadeOutManager.setFadeManagerConfiguration(fadeMgrConfig);
+    }
+
+    int clearFadeManagerConfiguration(int focusType) {
+        return mFadeOutManager.clearFadeManagerConfiguration();
+    }
+
+    FadeManagerConfiguration getFadeManagerConfiguration(int focusType) {
+        return mFadeOutManager.getFadeManagerConfiguration();
+    }
+
+    int setTransientFadeManagerConfiguration(int focusType,
+            FadeManagerConfiguration fadeMgrConfig) {
+        return mFadeOutManager.setTransientFadeManagerConfiguration(fadeMgrConfig);
+    }
+
+    int clearTransientFadeManagerConfiguration(int focusType) {
+        return mFadeOutManager.clearTransientFadeManagerConfiguration();
+    }
+
     /**
      * Inner class to track clients that want to be notified of playback updates
      */
@@ -1337,6 +1363,38 @@
         }
     }
 
+    static final class FadeEvent extends EventLogger.Event {
+        private final int mPlayerIId;
+        private final int mPlayerType;
+        private final int mClientUid;
+        private final int mClientPid;
+        private final AudioAttributes mPlayerAttr;
+        private final VolumeShaper.Configuration mVShaper;
+        private final VolumeShaper.Operation mVOperation;
+
+        FadeEvent(AudioPlaybackConfiguration apc, VolumeShaper.Configuration vShaper,
+                VolumeShaper.Operation vOperation) {
+            mPlayerIId = apc.getPlayerInterfaceId();
+            mClientUid = apc.getClientUid();
+            mClientPid = apc.getClientPid();
+            mPlayerAttr = apc.getAudioAttributes();
+            mPlayerType = apc.getPlayerType();
+            mVShaper = vShaper;
+            mVOperation = vOperation;
+        }
+
+        @Override
+        public String eventToString() {
+            return "Fade Event:" + " player piid:" + mPlayerIId
+                    + " uid/pid:" + mClientUid + "/" + mClientPid
+                    + " player type:"
+                    + AudioPlaybackConfiguration.toLogFriendlyPlayerType(mPlayerType)
+                    + " attr:" + mPlayerAttr
+                    + " volume shaper:" + mVShaper
+                    + " volume operation:" + mVOperation;
+        }
+    }
+
     private abstract static class VolumeShaperEvent extends EventLogger.Event {
         private final int mPlayerIId;
         private final boolean mSkipRamp;
diff --git a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
index f1d42f3..4a29eca 100644
--- a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
+++ b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
@@ -79,4 +79,11 @@
      * @return delay in milliseconds
      */
     long getFadeInDelayForOffendersMillis(@NonNull AudioAttributes aa);
+
+    /**
+     * Check if the fade should be enforced
+     *
+     * @return {@code true} if fade should be enforced, {@code false} otherwise
+     */
+    boolean shouldEnforceFade();
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 4f7f31d..8428f12 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -1639,8 +1639,7 @@
             return  -1;
         }
         final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
-        List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
-
+        List<String> deviceAddresses = mAudioService.getDeviceIdentityAddresses(currentDevice);
         // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
         // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
         // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index dafea9a..d5d8fd2 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -51,9 +51,13 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorPropertiesInternal;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorProperties;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.IFaceService;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintService;
@@ -122,8 +126,6 @@
 
         /**
          * Allows to test with various device sensor configurations.
-         * @param context
-         * @return
          */
         @VisibleForTesting
         public String[] getConfiguration(Context context) {
@@ -131,6 +133,30 @@
         }
 
         /**
+         * Allows to test with various device sensor configurations.
+         */
+        @VisibleForTesting
+        public String[] getFingerprintConfiguration(Context context) {
+            return getConfiguration(context);
+        }
+
+        /**
+         * Allows to test with various device sensor configurations.
+         */
+        @VisibleForTesting
+        public String[] getFaceConfiguration(Context context) {
+            return getConfiguration(context);
+        }
+
+        /**
+         * Allows to test with various device sensor configurations.
+         */
+        @VisibleForTesting
+        public String[] getIrisConfiguration(Context context) {
+            return getConfiguration(context);
+        }
+
+        /**
          * Allows us to mock FingerprintService for testing
          */
         @VisibleForTesting
@@ -173,6 +199,22 @@
             }
             return false;
         }
+
+        /**
+         * Allows to test with various fingerprint aidl instances.
+         */
+        @VisibleForTesting
+        public String[] getFingerprintAidlInstances() {
+            return ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR);
+        }
+
+        /**
+         * Allows to test with various face aidl instances.
+         */
+        @VisibleForTesting
+        public String[] getFaceAidlInstances() {
+            return ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR);
+        }
     }
 
     private final class AuthServiceImpl extends IAuthService.Stub {
@@ -717,12 +759,129 @@
             hidlConfigs = null;
         }
 
-        // Registers HIDL and AIDL authenticators, but only HIDL configs need to be provided.
-        registerAuthenticators(hidlConfigs);
+        if (com.android.server.biometrics.Flags.deHidl()) {
+            Slog.d(TAG, "deHidl flag is on.");
+            registerAuthenticators();
+        } else {
+            // Registers HIDL and AIDL authenticators, but only HIDL configs need to be provided.
+            registerAuthenticators(hidlConfigs);
+        }
 
         mInjector.publishBinderService(this, mImpl);
     }
 
+    private void registerAuthenticators() {
+        registerFingerprintSensors(mInjector.getFingerprintAidlInstances(),
+                mInjector.getFingerprintConfiguration(getContext()));
+        registerFaceSensors(mInjector.getFaceAidlInstances(),
+                mInjector.getFaceConfiguration(getContext()));
+        registerIrisSensors(mInjector.getIrisConfiguration(getContext()));
+    }
+
+    private void registerIrisSensors(String[] hidlConfigStrings) {
+        final SensorConfig[] hidlConfigs;
+        if (!mInjector.isHidlDisabled(getContext())) {
+            final int firstApiLevel = SystemProperties.getInt(SYSPROP_FIRST_API_LEVEL, 0);
+            final int apiLevel = SystemProperties.getInt(SYSPROP_API_LEVEL, firstApiLevel);
+            if (hidlConfigStrings.length == 0 && apiLevel == Build.VERSION_CODES.R) {
+                // For backwards compatibility with R where biometrics could work without being
+                // configured in config_biometric_sensors. In the absence of a vendor provided
+                // configuration, we assume the weakest biometric strength (i.e. convenience).
+                Slog.w(TAG, "Found R vendor partition without config_biometric_sensors");
+                hidlConfigStrings = generateRSdkCompatibleConfiguration();
+            }
+            hidlConfigs = new SensorConfig[hidlConfigStrings.length];
+            for (int i = 0; i < hidlConfigStrings.length; ++i) {
+                hidlConfigs[i] = new SensorConfig(hidlConfigStrings[i]);
+            }
+        } else {
+            hidlConfigs = null;
+        }
+
+        final List<SensorPropertiesInternal> hidlIrisSensors = new ArrayList<>();
+
+        if (hidlConfigs != null) {
+            for (SensorConfig sensor : hidlConfigs) {
+                switch (sensor.modality) {
+                    case TYPE_IRIS:
+                        hidlIrisSensors.add(getHidlIrisSensorProps(sensor.id, sensor.strength));
+                        break;
+
+                    default:
+                        Slog.e(TAG, "Unknown modality: " + sensor.modality);
+                }
+            }
+        }
+
+        final IIrisService irisService = mInjector.getIrisService();
+        if (irisService != null) {
+            try {
+                irisService.registerAuthenticators(hidlIrisSensors);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "RemoteException when registering iris authenticators", e);
+            }
+        } else if (hidlIrisSensors.size() > 0) {
+            Slog.e(TAG, "HIDL iris configuration exists, but IrisService is null.");
+        }
+    }
+
+    private void registerFaceSensors(final String[] faceAidlInstances,
+            final String[] hidlConfigStrings) {
+        final FaceSensorConfigurations mFaceSensorConfigurations =
+                new FaceSensorConfigurations(hidlConfigStrings != null
+                        && hidlConfigStrings.length > 0);
+
+        if (hidlConfigStrings != null && hidlConfigStrings.length > 0) {
+            mFaceSensorConfigurations.addHidlConfigs(
+                    hidlConfigStrings, getContext());
+        }
+
+        if (faceAidlInstances != null && faceAidlInstances.length > 0) {
+            mFaceSensorConfigurations.addAidlConfigs(faceAidlInstances,
+                    name -> IFace.Stub.asInterface(Binder.allowBlocking(
+                            ServiceManager.waitForDeclaredService(name))));
+        }
+
+        final IFaceService faceService = mInjector.getFaceService();
+        if (faceService != null) {
+            try {
+                faceService.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "RemoteException when registering face authenticators", e);
+            }
+        }  else if (mFaceSensorConfigurations.hasSensorConfigurations()) {
+            Slog.e(TAG, "Face configuration exists, but FaceService is null.");
+        }
+    }
+
+    private void registerFingerprintSensors(final String[] fingerprintAidlInstances,
+            final String[] hidlConfigStrings) {
+        final FingerprintSensorConfigurations mFingerprintSensorConfigurations =
+                new FingerprintSensorConfigurations(!(hidlConfigStrings != null
+                        && hidlConfigStrings.length > 0));
+
+        if (hidlConfigStrings != null && hidlConfigStrings.length > 0) {
+            mFingerprintSensorConfigurations.addHidlSensors(hidlConfigStrings, getContext());
+        }
+
+        if (fingerprintAidlInstances != null && fingerprintAidlInstances.length > 0) {
+            mFingerprintSensorConfigurations.addAidlSensors(fingerprintAidlInstances,
+                    name -> IFingerprint.Stub.asInterface(Binder.allowBlocking(
+                            ServiceManager.waitForDeclaredService(name))));
+        }
+
+        final IFingerprintService fingerprintService = mInjector.getFingerprintService();
+        if (fingerprintService != null) {
+            try {
+                fingerprintService.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "RemoteException when registering fingerprint authenticators", e);
+            }
+        }  else if (mFingerprintSensorConfigurations.hasSensorConfigurations()) {
+            Slog.e(TAG, "Fingerprint configuration exists, but FingerprintService is null.");
+        }
+    }
+
     /**
      * Generates an array of string configs with entries that correspond to the biometric features
      * declared on the device. Returns an empty array if no biometric features are declared.
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index 037ea38a..89b638b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -202,7 +202,7 @@
     };
 
     @VisibleForTesting
-    BiometricScheduler(@NonNull String tag,
+    public BiometricScheduler(@NonNull String tag,
             @NonNull Handler handler,
             @SensorType int sensorType,
             @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index 57feedc..0c3dfa7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -387,6 +387,11 @@
         return isAuthentication || isDetection;
     }
 
+    /** If this operation is {@link StartUserClient}. */
+    public boolean isStartUserOperation() {
+        return mClientMonitor instanceof StartUserClient<?, ?>;
+    }
+
     /** If this operation performs acquisition {@link AcquisitionClient}. */
     public boolean isAcquisitionOperation() {
         return mClientMonitor instanceof AcquisitionClient;
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
index 7f8f38f..6daaad1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
@@ -54,8 +54,6 @@
         // is all done internally.
         super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner,
                 0 /* cookie */, sensorId, logger, biometricContext);
-        //, BiometricsProtoEnums.ACTION_ENUMERATE,
-          //      BiometricsProtoEnums.CLIENT_UNKNOWN);
         mEnrolledList = enrolledList;
         mUtils = utils;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
index 8075470..3753bbd 100644
--- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
@@ -135,7 +135,7 @@
         final int currentUserId = mCurrentUserRetriever.getCurrentUserId();
         final int nextUserId = mPendingOperations.getFirst().getTargetUserId();
 
-        if (nextUserId == currentUserId) {
+        if (nextUserId == currentUserId || mPendingOperations.getFirst().isStartUserOperation()) {
             super.startNextOperationIfIdle();
         } else if (currentUserId == UserHandle.USER_NULL) {
             final BaseClientMonitor startClient =
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 578d9dc..0f964bb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -36,6 +36,7 @@
 import android.hardware.biometrics.face.SensorProps;
 import android.hardware.face.Face;
 import android.hardware.face.FaceAuthenticateOptions;
+import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.FaceServiceReceiver;
 import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
@@ -55,6 +56,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.Surface;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
@@ -76,6 +78,8 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
 
 /**
  * A service to manage multiple clients that want to access the face HAL API.
@@ -86,7 +90,7 @@
 
     protected static final String TAG = "FaceService";
 
-    private final FaceServiceWrapper mServiceWrapper;
+    @VisibleForTesting final FaceServiceWrapper mServiceWrapper;
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final LockPatternUtils mLockPatternUtils;
     @NonNull
@@ -94,11 +98,21 @@
     @NonNull
     private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal>
             mBiometricStateCallback;
+    @NonNull
+    private final FaceProviderFunction mFaceProviderFunction;
+    @NonNull private final Function<String, FaceProvider> mFaceProvider;
+    @NonNull
+    private final Supplier<String[]> mAidlInstanceNameSupplier;
+
+    interface FaceProviderFunction {
+        FaceProvider getFaceProvider(Pair<String, SensorProps[]> filteredSensorProps,
+                boolean resetLockoutRequiresChallenge);
+    }
 
     /**
      * Receives the incoming binder calls from FaceManager.
      */
-    private final class FaceServiceWrapper extends IFaceService.Stub {
+    @VisibleForTesting final class FaceServiceWrapper extends IFaceService.Stub {
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
@@ -661,22 +675,9 @@
             final List<ServiceProvider> providers = new ArrayList<>();
 
             for (String instance : instances) {
-                final String fqName = IFace.DESCRIPTOR + "/" + instance;
-                final IFace face = IFace.Stub.asInterface(
-                        Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName)));
-                if (face == null) {
-                    Slog.e(TAG, "Unable to get declared service: " + fqName);
-                    continue;
-                }
-                try {
-                    final SensorProps[] props = face.getSensorProps();
-                    final FaceProvider provider = new FaceProvider(getContext(),
-                            mBiometricStateCallback, props, instance, mLockoutResetDispatcher,
-                            BiometricContext.getInstance(getContext()));
-                    providers.add(provider);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
-                }
+                final FaceProvider provider = mFaceProvider.apply(instance);
+                Slog.i(TAG, "Adding AIDL provider: " + instance);
+                providers.add(provider);
             }
 
             return providers;
@@ -689,7 +690,7 @@
 
             mRegistry.registerAll(() -> {
                 List<String> aidlSensors = new ArrayList<>();
-                final String[] instances = ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR);
+                final String[] instances = mAidlInstanceNameSupplier.get();
                 if (instances != null) {
                     aidlSensors.addAll(Lists.newArrayList(instances));
                 }
@@ -704,6 +705,55 @@
             });
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        public void registerAuthenticatorsLegacy(
+                FaceSensorConfigurations faceSensorConfigurations) {
+            super.registerAuthenticatorsLegacy_enforcePermission();
+
+            if (!faceSensorConfigurations.hasSensorConfigurations()) {
+                Slog.d(TAG, "No face sensors to register.");
+                return;
+            }
+            mRegistry.registerAll(() -> getProviders(faceSensorConfigurations));
+        }
+
+        private List<ServiceProvider> getProviders(
+                FaceSensorConfigurations faceSensorConfigurations) {
+            final List<ServiceProvider> providers = new ArrayList<>();
+            final Pair<String, SensorProps[]> filteredSensorProps =
+                    filterAvailableHalInstances(faceSensorConfigurations);
+            providers.add(mFaceProviderFunction.getFaceProvider(filteredSensorProps,
+                    faceSensorConfigurations.getResetLockoutRequiresChallenge()));
+            return providers;
+        }
+
+        @NonNull
+        private Pair<String, SensorProps[]> filterAvailableHalInstances(
+                FaceSensorConfigurations faceSensorConfigurations) {
+            Pair<String, SensorProps[]> finalSensorPair = faceSensorConfigurations.getSensorPair();
+
+            if (faceSensorConfigurations.isSingleSensorConfigurationPresent()) {
+                return finalSensorPair;
+            }
+
+            final Pair<String, SensorProps[]> virtualSensorProps = faceSensorConfigurations
+                    .getSensorPairForInstance("virtual");
+
+            if (Utils.isVirtualEnabled(getContext())) {
+                if (virtualSensorProps != null) {
+                    return virtualSensorProps;
+                } else {
+                    Slog.e(TAG, "Could not find virtual interface while it is enabled");
+                    return finalSensorPair;
+                }
+            } else {
+                if (virtualSensorProps != null) {
+                    return faceSensorConfigurations.getSensorPairNotForInstance("virtual");
+                }
+            }
+            return finalSensorPair;
+        }
+
         private Pair<List<FaceSensorPropertiesInternal>, List<String>>
                 filterAvailableHalInstances(
                 @NonNull List<FaceSensorPropertiesInternal> hidlInstances,
@@ -752,20 +802,62 @@
     }
 
     public FaceService(Context context) {
+        this(context, null /* faceProviderFunction */, () -> IBiometricService.Stub.asInterface(
+                ServiceManager.getService(Context.BIOMETRIC_SERVICE)), null /* faceProvider */,
+                () -> ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR));
+    }
+
+    @VisibleForTesting FaceService(Context context,
+            FaceProviderFunction faceProviderFunction,
+            Supplier<IBiometricService> biometricServiceSupplier,
+            Function<String, FaceProvider> faceProvider,
+            Supplier<String[]> aidlInstanceNameSupplier) {
         super(context);
         mServiceWrapper = new FaceServiceWrapper();
         mLockoutResetDispatcher = new LockoutResetDispatcher(context);
         mLockPatternUtils = new LockPatternUtils(context);
         mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context));
-        mRegistry = new FaceServiceRegistry(mServiceWrapper,
-                () -> IBiometricService.Stub.asInterface(
-                        ServiceManager.getService(Context.BIOMETRIC_SERVICE)));
+        mRegistry = new FaceServiceRegistry(mServiceWrapper, biometricServiceSupplier);
         mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
             @Override
             public void onAllAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) {
                 mBiometricStateCallback.start(mRegistry.getProviders());
             }
         });
+        mAidlInstanceNameSupplier = aidlInstanceNameSupplier;
+
+        mFaceProvider = faceProvider != null ? faceProvider : (name) -> {
+            final String fqName = IFace.DESCRIPTOR + "/" + name;
+            final IFace face = IFace.Stub.asInterface(
+                    Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName)));
+            if (face == null) {
+                Slog.e(TAG, "Unable to get declared service: " + fqName);
+                return null;
+            }
+            try {
+                final SensorProps[] props = face.getSensorProps();
+                return new FaceProvider(getContext(),
+                        mBiometricStateCallback, props, name, mLockoutResetDispatcher,
+                        BiometricContext.getInstance(getContext()),
+                        false /* resetLockoutRequiresChallenge */);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
+            }
+
+            return null;
+        };
+
+        if (Flags.deHidl()) {
+            mFaceProviderFunction = faceProviderFunction != null ? faceProviderFunction :
+                    ((filteredSensorProps, resetLockoutRequiresChallenge) -> new FaceProvider(
+                            getContext(), mBiometricStateCallback,
+                            filteredSensorProps.second,
+                            filteredSensorProps.first, mLockoutResetDispatcher,
+                            BiometricContext.getInstance(getContext()),
+                            resetLockoutRequiresChallenge));
+        } else {
+            mFaceProviderFunction = ((filteredSensorProps, resetLockoutRequiresChallenge) -> null);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java
index e5d4a63..ef2be79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/LockoutHalImpl.java
@@ -24,7 +24,8 @@
  * the user changes.
  */
 public class LockoutHalImpl implements LockoutTracker {
-    private @LockoutMode int mCurrentUserLockoutMode;
+    @LockoutMode
+    private int mCurrentUserLockoutMode;
 
     @Override
     public int getLockoutModeForUser(int userId) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java
index 57f5b34..098be21 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java
@@ -27,6 +27,7 @@
 import android.hardware.keymaster.HardwareAuthToken;
 import android.util.Slog;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.AcquisitionClient;
@@ -59,6 +60,20 @@
         void onHardwareUnavailable();
     }
 
+    /**
+     * Interface to send results to the AidlResponseHandler's owner.
+     */
+    public interface AidlResponseHandlerCallback {
+        /**
+         * Invoked when enrollment is successful.
+         */
+        void onEnrollSuccess();
+        /**
+         * Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
+         */
+        void onHardwareUnavailable();
+    }
+
     private static final String TAG = "AidlResponseHandler";
 
     @NonNull
@@ -68,7 +83,7 @@
     private final int mSensorId;
     private final int mUserId;
     @NonNull
-    private final LockoutTracker mLockoutCache;
+    private final LockoutTracker mLockoutTracker;
     @NonNull
     private final LockoutResetDispatcher mLockoutResetDispatcher;
 
@@ -76,6 +91,8 @@
     private final AuthSessionCoordinator mAuthSessionCoordinator;
     @NonNull
     private final HardwareUnavailableCallback mHardwareUnavailableCallback;
+    @NonNull
+    private final AidlResponseHandlerCallback mAidlResponseHandlerCallback;
 
     public AidlResponseHandler(@NonNull Context context,
             @NonNull BiometricScheduler scheduler, int sensorId, int userId,
@@ -83,14 +100,33 @@
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull AuthSessionCoordinator authSessionCoordinator,
             @NonNull HardwareUnavailableCallback hardwareUnavailableCallback) {
+        this(context, scheduler, sensorId, userId, lockoutTracker, lockoutResetDispatcher,
+                authSessionCoordinator, hardwareUnavailableCallback,
+                new AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {}
+
+                    @Override
+                    public void onHardwareUnavailable() {}
+                });
+    }
+
+    public AidlResponseHandler(@NonNull Context context,
+            @NonNull BiometricScheduler scheduler, int sensorId, int userId,
+            @NonNull LockoutTracker lockoutTracker,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @NonNull HardwareUnavailableCallback hardwareUnavailableCallback,
+            @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) {
         mContext = context;
         mScheduler = scheduler;
         mSensorId = sensorId;
         mUserId = userId;
-        mLockoutCache = lockoutTracker;
+        mLockoutTracker = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mAuthSessionCoordinator = authSessionCoordinator;
         mHardwareUnavailableCallback = hardwareUnavailableCallback;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback;
     }
 
     @Override
@@ -106,13 +142,13 @@
     @Override
     public void onChallengeGenerated(long challenge) {
         handleResponse(FaceGenerateChallengeClient.class, (c) -> c.onChallengeGenerated(mSensorId,
-                mUserId, challenge), null);
+                mUserId, challenge));
     }
 
     @Override
     public void onChallengeRevoked(long challenge) {
         handleResponse(FaceRevokeChallengeClient.class, (c) -> c.onChallengeRevoked(mSensorId,
-                mUserId, challenge), null);
+                mUserId, challenge));
     }
 
     @Override
@@ -123,7 +159,7 @@
                 return;
             }
             c.onAuthenticationFrame(AidlConversionUtils.toFrameworkAuthenticationFrame(frame));
-        }, null);
+        });
     }
 
     @Override
@@ -134,7 +170,7 @@
                 return;
             }
             c.onEnrollmentFrame(AidlConversionUtils.toFrameworkEnrollmentFrame(frame));
-        }, null);
+        });
     }
 
     @Override
@@ -149,9 +185,13 @@
         handleResponse(ErrorConsumer.class, (c) -> {
             c.onError(error, vendorCode);
             if (error == Error.HW_UNAVAILABLE) {
-                mHardwareUnavailableCallback.onHardwareUnavailable();
+                if (Flags.deHidl()) {
+                    mAidlResponseHandlerCallback.onHardwareUnavailable();
+                } else {
+                    mHardwareUnavailableCallback.onHardwareUnavailable();
+                }
             }
-        }, null);
+        });
     }
 
     @Override
@@ -167,7 +207,12 @@
                 .getUniqueName(mContext, currentUserId);
         final Face face = new Face(name, enrollmentId, mSensorId);
 
-        handleResponse(FaceEnrollClient.class, (c) -> c.onEnrollResult(face, remaining), null);
+        handleResponse(FaceEnrollClient.class, (c) -> {
+            c.onEnrollResult(face, remaining);
+            if (remaining == 0) {
+                mAidlResponseHandlerCallback.onEnrollSuccess();
+            }
+        });
     }
 
     @Override
@@ -179,37 +224,37 @@
             byteList.add(b);
         }
         handleResponse(AuthenticationConsumer.class, (c) -> c.onAuthenticated(face,
-                true /* authenticated */, byteList), null);
+                true /* authenticated */, byteList));
     }
 
     @Override
     public void onAuthenticationFailed() {
         final Face face = new Face("" /* name */, 0 /* faceId */, mSensorId);
         handleResponse(AuthenticationConsumer.class, (c) -> c.onAuthenticated(face,
-                false /* authenticated */, null /* hat */), null);
+                false /* authenticated */, null /* hat */));
     }
 
     @Override
     public void onLockoutTimed(long durationMillis) {
-        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis), null);
+        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis));
     }
 
     @Override
     public void onLockoutPermanent() {
-        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent, null);
+        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent);
     }
 
     @Override
     public void onLockoutCleared() {
         handleResponse(FaceResetLockoutClient.class, FaceResetLockoutClient::onLockoutCleared,
                 (c) -> FaceResetLockoutClient.resetLocalLockoutStateToNone(mSensorId, mUserId,
-                mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator,
-                Utils.getCurrentStrength(mSensorId), -1 /* requestId */));
+                        mLockoutTracker, mLockoutResetDispatcher, mAuthSessionCoordinator,
+                        Utils.getCurrentStrength(mSensorId), -1 /* requestId */));
     }
 
     @Override
     public void onInteractionDetected() {
-        handleResponse(FaceDetectClient.class, FaceDetectClient::onInteractionDetected, null);
+        handleResponse(FaceDetectClient.class, FaceDetectClient::onInteractionDetected);
     }
 
     @Override
@@ -219,23 +264,23 @@
                 final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId);
                 final int finalI = i;
                 handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(face,
-                        enrollmentIds.length - finalI - 1), null);
+                        enrollmentIds.length - finalI - 1 /* remaining */));
             }
         } else {
             handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(
-                    null /* identifier */, 0 /* remaining */), null);
+                    null /* identifier */, 0 /* remaining */));
         }
     }
 
     @Override
     public void onFeaturesRetrieved(byte[] features) {
         handleResponse(FaceGetFeatureClient.class, (c) -> c.onFeatureGet(true /* success */,
-                features), null);
+                features));
     }
 
     @Override
     public void onFeatureSet(byte feature) {
-        handleResponse(FaceSetFeatureClient.class, (c) -> c.onFeatureSet(true /* success */), null);
+        handleResponse(FaceSetFeatureClient.class, (c) -> c.onFeatureSet(true /* success */));
     }
 
     @Override
@@ -245,33 +290,32 @@
                 final Face face = new Face("" /* name */, enrollmentIds[i], mSensorId);
                 final int finalI = i;
                 handleResponse(RemovalConsumer.class,
-                        (c) -> c.onRemoved(face, enrollmentIds.length - finalI - 1),
-                        null);
+                        (c) -> c.onRemoved(face,
+                                enrollmentIds.length - finalI - 1 /* remaining */));
             }
         } else {
             handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null /* identifier */,
-                    0 /* remaining */), null);
+                    0 /* remaining */));
         }
     }
 
     @Override
     public void onAuthenticatorIdRetrieved(long authenticatorId) {
         handleResponse(FaceGetAuthenticatorIdClient.class, (c) -> c.onAuthenticatorIdRetrieved(
-                authenticatorId), null);
+                authenticatorId));
     }
 
     @Override
     public void onAuthenticatorIdInvalidated(long newAuthenticatorId) {
         handleResponse(FaceInvalidationClient.class, (c) -> c.onAuthenticatorIdInvalidated(
-                newAuthenticatorId), null);
+                newAuthenticatorId));
     }
 
     /**
      * Handles acquired messages sent by the HAL (specifically for HIDL HAL).
      */
     public void onAcquired(int acquiredInfo, int vendorCode) {
-        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode),
-                null);
+        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode));
     }
 
     /**
@@ -288,7 +332,7 @@
                 lockoutMode = LockoutTracker.LOCKOUT_TIMED;
             }
 
-            mLockoutCache.setLockoutModeForUser(mUserId, lockoutMode);
+            mLockoutTracker.setLockoutModeForUser(mUserId, lockoutMode);
 
             if (duration == 0) {
                 mLockoutResetDispatcher.notifyLockoutResetCallbacks(mSensorId);
@@ -296,6 +340,20 @@
         });
     }
 
+    /**
+     * Handle clients which are not supported in HIDL HAL. For face, FaceInvalidationClient
+     * is the only AIDL client which is not supported in HIDL.
+     */
+    public void onUnsupportedClientScheduled() {
+        Slog.e(TAG, "FaceInvalidationClient is not supported in the HAL.");
+        handleResponse(FaceInvalidationClient.class, BaseClientMonitor::cancel);
+    }
+
+    private <T> void handleResponse(@NonNull Class<T> className,
+            @NonNull Consumer<T> action) {
+        handleResponse(className, action, null /* alternateAction */);
+    }
+
     private <T> void handleResponse(@NonNull Class<T> className,
             @NonNull Consumer<T> actionIfClassMatchesClient,
             @Nullable Consumer<BaseClientMonitor> alternateAction) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java
index e70e25a..af46f44 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java
@@ -21,7 +21,7 @@
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.V1_0.IBiometricsFace;
 
-import com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter;
 
 import java.util.function.Supplier;
 
@@ -47,7 +47,7 @@
 
     public AidlSession(Context context, Supplier<IBiometricsFace> session, int userId,
             AidlResponseHandler aidlResponseHandler) {
-        mSession = new AidlToHidlAdapter(context, session, userId, aidlResponseHandler);
+        mSession = new HidlToAidlSessionAdapter(context, session, userId, aidlResponseHandler);
         mHalInterfaceVersion = 0;
         mUserId = userId;
         mAidlResponseHandler = aidlResponseHandler;
@@ -64,7 +64,7 @@
     }
 
     /** The HAL callback, which should only be used in tests {@See BiometricTestSessionImpl}. */
-    AidlResponseHandler getHalSessionCallback() {
+    public AidlResponseHandler getHalSessionCallback() {
         return mAidlResponseHandler;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java
index c15049b..c41b706 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java
@@ -34,7 +34,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.HalClientMonitor;
-import com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -75,8 +75,8 @@
     protected void startHalOperation() {
         try {
             ISession session = getFreshDaemon().getSession();
-            if (session instanceof AidlToHidlAdapter) {
-                ((AidlToHidlAdapter) session).setFeature(mFeature);
+            if (session instanceof HidlToAidlSessionAdapter) {
+                ((HidlToAidlSessionAdapter) session).setFeature(mFeature);
             }
             session.getFeatures();
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index dd9c6d5..9fa15b8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -24,6 +24,7 @@
 import android.app.TaskStackListener;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.hardware.biometrics.BiometricFaceConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.IInvalidationCallback;
@@ -51,6 +52,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
 import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -64,11 +66,13 @@
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.SensorList;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 import com.android.server.biometrics.sensors.face.ServiceProvider;
 import com.android.server.biometrics.sensors.face.UsageStats;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSensorAdapter;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -152,9 +156,11 @@
             @NonNull SensorProps[] props,
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext) {
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge) {
         this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                biometricContext, null /* daemon */);
+                biometricContext, null /* daemon */, resetLockoutRequiresChallenge,
+                false /* testHalEnabled */);
     }
 
     @VisibleForTesting FaceProvider(@NonNull Context context,
@@ -163,7 +169,8 @@
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull BiometricContext biometricContext,
-            IFace daemon) {
+            @Nullable IFace daemon, boolean resetLockoutRequiresChallenge,
+            boolean testHalEnabled) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
@@ -176,48 +183,118 @@
         mBiometricContext = biometricContext;
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
         mDaemon = daemon;
+        mTestHalEnabled = testHalEnabled;
 
-        AuthenticationStatsBroadcastReceiver mBroadcastReceiver =
-                new AuthenticationStatsBroadcastReceiver(
-                        mContext,
-                        BiometricsProtoEnums.MODALITY_FACE,
-                        (AuthenticationStatsCollector collector) -> {
-                            Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
-                            mAuthenticationStatsCollector = collector;
-                        });
+        initAuthenticationBroadcastReceiver();
+        initSensors(resetLockoutRequiresChallenge, props);
+    }
 
-        for (SensorProps prop : props) {
-            final int sensorId = prop.commonProps.sensorId;
+    private void initAuthenticationBroadcastReceiver() {
+        new AuthenticationStatsBroadcastReceiver(
+                mContext,
+                BiometricsProtoEnums.MODALITY_FACE,
+                (AuthenticationStatsCollector collector) -> {
+                    Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
+                    mAuthenticationStatsCollector = collector;
+                });
+    }
 
-            final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
-            if (prop.commonProps.componentInfo != null) {
-                for (ComponentInfo info : prop.commonProps.componentInfo) {
-                    componentInfo.add(new ComponentInfoInternal(info.componentId,
-                            info.hardwareVersion, info.firmwareVersion, info.serialNumber,
-                            info.softwareVersion));
+    private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) {
+        if (Flags.deHidl()) {
+            if (resetLockoutRequiresChallenge) {
+                Slog.d(getTag(), "Adding HIDL configs");
+                for (SensorProps prop : props) {
+                    addHidlSensors(prop, resetLockoutRequiresChallenge);
+                }
+            } else {
+                Slog.d(getTag(), "Adding AIDL configs");
+                for (SensorProps prop : props) {
+                    addAidlSensors(prop, resetLockoutRequiresChallenge);
                 }
             }
+        } else {
+            for (SensorProps prop : props) {
+                final int sensorId = prop.commonProps.sensorId;
 
-            final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
-                    prop.commonProps.sensorId, prop.commonProps.sensorStrength,
-                    prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
-                    prop.supportsDetectInteraction, prop.halControlsPreview,
-                    false /* resetLockoutRequiresChallenge */);
-            final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler,
-                    internalProp, lockoutResetDispatcher, mBiometricContext);
-            final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
-                    sensor.getLazySession().get().getUserId();
-            mFaceSensors.addSensor(sensorId, sensor, userId,
-                    new SynchronousUserSwitchObserver() {
-                        @Override
-                        public void onUserSwitching(int newUserId) {
-                            scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
-                        }
-                    });
-            Slog.d(getTag(), "Added: " + internalProp);
+                final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+                if (prop.commonProps.componentInfo != null) {
+                    for (ComponentInfo info : prop.commonProps.componentInfo) {
+                        componentInfo.add(new ComponentInfoInternal(info.componentId,
+                                info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                                info.softwareVersion));
+                    }
+                }
+
+                final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+                        prop.commonProps.sensorId, prop.commonProps.sensorStrength,
+                        prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
+                        prop.supportsDetectInteraction, prop.halControlsPreview,
+                        false /* resetLockoutRequiresChallenge */);
+                final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this,
+                        mContext, mHandler, internalProp, mLockoutResetDispatcher,
+                        mBiometricContext);
+                sensor.init(mLockoutResetDispatcher, this);
+                final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                        sensor.getLazySession().get().getUserId();
+                mFaceSensors.addSensor(sensorId, sensor, userId,
+                        new SynchronousUserSwitchObserver() {
+                            @Override
+                            public void onUserSwitching(int newUserId) {
+                                scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                            }
+                        });
+                Slog.d(getTag(), "Added: " + internalProp);
+            }
         }
     }
 
+    private void addHidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/" + sensorId, this,
+                mContext, mHandler, prop, mLockoutResetDispatcher,
+                mBiometricContext, resetLockoutRequiresChallenge,
+                () -> {
+                    //TODO: update to make this testable
+                    scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(),
+                            null /* callback */);
+                    scheduleGetFeature(sensorId, new Binder(), ActivityManager.getCurrentUser(),
+                            BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION, null,
+                            mContext.getOpPackageName());
+                });
+        sensor.init(mLockoutResetDispatcher, this);
+        final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFaceSensors.addSensor(sensorId, sensor, userId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                        scheduleGetFeature(sensorId, new Binder(), newUserId,
+                                BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION,
+                                null, mContext.getOpPackageName());
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFaceSensors.get(sensorId));
+    }
+
+    private void addAidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext,
+                mHandler, prop, mLockoutResetDispatcher, mBiometricContext,
+                resetLockoutRequiresChallenge);
+        sensor.init(mLockoutResetDispatcher, this);
+        final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFaceSensors.addSensor(sensorId, sensor, userId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFaceSensors.get(sensorId));
+    }
+
     private String getTag() {
         return "FaceProvider/" + mHalInstanceName;
     }
@@ -290,7 +367,10 @@
         }
     }
 
-    private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
+    /**
+     * Schedules FaceGetAuthenticatorIdClient for specific sensor and user.
+     */
+    protected void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
         mHandler.post(() -> {
             final FaceGetAuthenticatorIdClient client = new FaceGetAuthenticatorIdClient(
                     mContext, mFaceSensors.get(sensorId).getLazySession(), userId,
@@ -365,8 +445,12 @@
 
     @Override
     public int getLockoutModeForUser(int sensorId, int userId) {
-        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
-                Utils.getCurrentStrength(sensorId));
+        if (Flags.deHidl()) {
+            return mFaceSensors.get(sensorId).getLockoutModeForUser(userId);
+        } else {
+            return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                    Utils.getCurrentStrength(sensorId));
+        }
     }
 
     @Override
@@ -376,13 +460,18 @@
 
     @Override
     public boolean isHardwareDetected(int sensorId) {
-        return hasHalInstance();
+        if (Flags.deHidl()) {
+            return mFaceSensors.get(sensorId).isHardwareDetected(mHalInstanceName);
+        } else {
+            return hasHalInstance();
+        }
     }
 
     @Override
     public void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token,
             @NonNull IFaceServiceReceiver receiver, String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceGenerateChallengeClient client = new FaceGenerateChallengeClient(mContext,
                     mFaceSensors.get(sensorId).getLazySession(), token,
                     new ClientMonitorCallbackConverter(receiver), userId, opPackageName, sensorId,
@@ -416,6 +505,7 @@
             @Nullable Surface previewSurface, boolean debugConsent) {
         final long id = mRequestCounter.incrementAndGet();
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final int maxTemplatesPerUser = mFaceSensors.get(
                     sensorId).getSensorProperties().maxEnrollmentsPerUser;
             final FaceEnrollClient client = new FaceEnrollClient(mContext,
@@ -427,18 +517,23 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext, maxTemplatesPerUser, debugConsent);
-            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
-                            mBiometricStateCallback, new ClientMonitorCallback() {
-                        @Override
-                        public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
-                                boolean success) {
-                            ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
-                            if (success) {
-                                scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
-                                scheduleInvalidationRequest(sensorId, userId);
+            if (Flags.deHidl()) {
+                scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            } else {
+                scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+                        mBiometricStateCallback, new ClientMonitorCallback() {
+                            @Override
+                            public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+                                    boolean success) {
+                                ClientMonitorCallback.super.onClientFinished(clientMonitor,
+                                        success);
+                                if (success) {
+                                    scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
+                                    scheduleInvalidationRequest(sensorId, userId);
+                                }
                             }
-                        }
-                    }));
+                        }));
+            }
         });
         return id;
     }
@@ -486,6 +581,13 @@
             final int userId = options.getUserId();
             final int sensorId = options.getSensorId();
             final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId);
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
+            final LockoutTracker lockoutTracker;
+            if (Flags.deHidl()) {
+                lockoutTracker = mFaceSensors.get(sensorId).getLockoutTracker(true /* forAuth */);
+            } else {
+                lockoutTracker = null;
+            }
             final FaceAuthenticationClient client = new FaceAuthenticationClient(
                     mContext, mFaceSensors.get(sensorId).getLazySession(), token, requestId,
                     callback, operationId, restricted, options, cookie,
@@ -493,7 +595,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
                             mAuthenticationStatsCollector),
                     mBiometricContext, isStrongBiometric,
-                    mUsageStats, null /* lockoutTracker */,
+                    mUsageStats, lockoutTracker,
                     allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId));
             scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
                 @Override
@@ -555,6 +657,7 @@
     private void scheduleRemoveSpecifiedIds(int sensorId, @NonNull IBinder token, int[] faceIds,
             int userId, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceRemovalClient client = new FaceRemovalClient(mContext,
                     mFaceSensors.get(sensorId).getLazySession(), token,
                     new ClientMonitorCallbackConverter(receiver), faceIds, userId,
@@ -571,6 +674,7 @@
     @Override
     public void scheduleResetLockout(int sensorId, int userId, @NonNull byte[] hardwareAuthToken) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceResetLockoutClient client = new FaceResetLockoutClient(
                     mContext, mFaceSensors.get(sensorId).getLazySession(), userId,
                     mContext.getOpPackageName(), sensorId,
@@ -578,8 +682,8 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken,
-                    mFaceSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher,
-                    Utils.getCurrentStrength(sensorId));
+                    mFaceSensors.get(sensorId).getLockoutTracker(false/* forAuth */),
+                    mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId));
 
             scheduleForSensor(sensorId, client);
         });
@@ -590,6 +694,7 @@
             boolean enabled, @NonNull byte[] hardwareAuthToken,
             @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final List<Face> faces = FaceUtils.getInstance(sensorId)
                     .getBiometricsForUser(mContext, userId);
             if (faces.isEmpty()) {
@@ -610,6 +715,7 @@
     public void scheduleGetFeature(int sensorId, @NonNull IBinder token, int userId, int feature,
             @NonNull ClientMonitorCallbackConverter callback, @NonNull String opPackageName) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final List<Face> faces = FaceUtils.getInstance(sensorId)
                     .getBiometricsForUser(mContext, userId);
             if (faces.isEmpty()) {
@@ -641,6 +747,7 @@
     public void scheduleInternalCleanup(int sensorId, int userId,
             @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments) {
         mHandler.post(() -> {
+            mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId);
             final FaceInternalCleanupClient client =
                     new FaceInternalCleanupClient(mContext,
                             mFaceSensors.get(sensorId).getLazySession(), userId,
@@ -760,4 +867,8 @@
         }
         biometricScheduler.startWatchdog();
     }
+
+    public boolean getTestHalEnabled() {
+        return mTestHalEnabled;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
index 77b5592..d02eefa 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.hardware.biometrics.BiometricManager.Authenticators;
 import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.ISession;
 import android.hardware.keymaster.HardwareAuthToken;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -34,6 +35,7 @@
 import com.android.server.biometrics.sensors.HalClientMonitor;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter;
 
 import java.util.function.Supplier;
 
@@ -47,7 +49,7 @@
     private static final String TAG = "FaceResetLockoutClient";
 
     private final HardwareAuthToken mHardwareAuthToken;
-    private final LockoutTracker mLockoutCache;
+    private final LockoutTracker mLockoutTracker;
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final int mBiometricStrength;
 
@@ -60,7 +62,7 @@
         super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
                 0 /* cookie */, sensorId, logger, biometricContext);
         mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken);
-        mLockoutCache = lockoutTracker;
+        mLockoutTracker = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mBiometricStrength = biometricStrength;
     }
@@ -79,7 +81,11 @@
     @Override
     protected void startHalOperation() {
         try {
-            getFreshDaemon().getSession().resetLockout(mHardwareAuthToken);
+            final ISession session = getFreshDaemon().getSession();
+            session.resetLockout(mHardwareAuthToken);
+            if (session instanceof HidlToAidlSessionAdapter) {
+                mCallback.onClientFinished(this, true /* success */);
+            }
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to reset lockout", e);
             mCallback.onClientFinished(this, false /* success */);
@@ -87,7 +93,7 @@
     }
 
     void onLockoutCleared() {
-        resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache,
+        resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutTracker,
                 mLockoutResetDispatcher, getBiometricContext().getAuthSessionCoordinator(),
                 mBiometricStrength, getRequestId());
         mCallback.onClientFinished(this, true /* success */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index 54e66eb..3e5c599 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -21,16 +21,20 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.hardware.biometrics.BiometricsProtoEnums;
+import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
+import android.hardware.biometrics.common.ComponentInfo;
 import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
+import android.hardware.biometrics.face.SensorProps;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Slog;
@@ -38,6 +42,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.SensorServiceStateProto;
 import com.android.server.biometrics.SensorStateProto;
 import com.android.server.biometrics.UserStateProto;
@@ -49,12 +54,15 @@
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.StartUserClient;
 import com.android.server.biometrics.sensors.StopUserClient;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
 
@@ -71,25 +79,53 @@
     @NonNull private final IBinder mToken;
     @NonNull private final Handler mHandler;
     @NonNull private final FaceSensorPropertiesInternal mSensorProperties;
-    @NonNull private final UserAwareBiometricScheduler mScheduler;
-    @NonNull private final LockoutCache mLockoutCache;
+    @NonNull private BiometricScheduler mScheduler;
+    @Nullable private LockoutTracker mLockoutTracker;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
 
-    @NonNull private final Supplier<AidlSession> mLazySession;
+    @NonNull private Supplier<AidlSession> mLazySession;
     @Nullable AidlSession mCurrentSession;
+    @NonNull BiometricContext mBiometricContext;
 
 
     Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
             @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext, AidlSession session) {
+            @NonNull BiometricContext biometricContext, @Nullable AidlSession session) {
         mTag = tag;
         mProvider = provider;
         mContext = context;
         mToken = new Binder();
         mHandler = handler;
         mSensorProperties = sensorProperties;
-        mScheduler = new UserAwareBiometricScheduler(tag,
+        mBiometricContext = biometricContext;
+        mAuthenticatorIds = new HashMap<>();
+    }
+
+    Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext) {
+        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+                biometricContext, null);
+    }
+
+    public Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge) {
+        this(tag, provider, context, handler,
+                getFaceSensorPropertiesInternal(prop, resetLockoutRequiresChallenge),
+                lockoutResetDispatcher, biometricContext, null);
+    }
+
+    /**
+     * Initialize biometric scheduler, lockout tracker and session for the sensor.
+     */
+    public void init(LockoutResetDispatcher lockoutResetDispatcher,
+            FaceProvider provider) {
+        mScheduler = new UserAwareBiometricScheduler(mTag,
                 BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityDispatcher */,
                 () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
                 new UserAwareBiometricScheduler.UserSwitchCallback() {
@@ -98,7 +134,7 @@
                     public StopUserClient<?> getStopUserClient(int userId) {
                         return new FaceStopUserClient(mContext, mLazySession, mToken, userId,
                                 mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 () -> mCurrentSession = null);
                     }
 
@@ -107,13 +143,36 @@
                     public StartUserClient<?, ?> getStartUserClient(int newUserId) {
                         final int sensorId = mSensorProperties.sensorId;
 
-                        final AidlResponseHandler resultController = new AidlResponseHandler(
-                                mContext, mScheduler, sensorId, newUserId,
-                                mLockoutCache, lockoutResetDispatcher,
-                                biometricContext.getAuthSessionCoordinator(), () -> {
-                            Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
-                            mCurrentSession = null;
-                        });
+                        final AidlResponseHandler resultController;
+                        if (Flags.deHidl()) {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {},
+                                    new AidlResponseHandler.AidlResponseHandlerCallback() {
+                                        @Override
+                                        public void onEnrollSuccess() {
+                                            mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId,
+                                                    newUserId);
+                                            mProvider.scheduleInvalidationRequest(sensorId,
+                                                    newUserId);
+                                        }
+
+                                        @Override
+                                        public void onHardwareUnavailable() {
+                                            Slog.e(mTag, "Face sensor hardware unavailable.");
+                                            mCurrentSession = null;
+                                        }
+                                    });
+                        } else {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {
+                                Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
+                                mCurrentSession = null;
+                            });
+                        }
 
                         final StartUserClient.UserStartedCallback<ISession> userStartedCallback =
                                 (userIdStarted, newSession, halInterfaceVersion) -> {
@@ -136,32 +195,42 @@
 
                         return new FaceStartUserClient(mContext, provider::getHalInstance,
                                 mToken, newUserId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 resultController, userStartedCallback);
                     }
                 });
-        mLockoutCache = new LockoutCache();
-        mAuthenticatorIds = new HashMap<>();
         mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+        mLockoutTracker = new LockoutCache();
     }
 
-    Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
-            @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
-            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull BiometricContext biometricContext) {
-        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
-                biometricContext, null);
+    private static FaceSensorPropertiesInternal getFaceSensorPropertiesInternal(SensorProps prop,
+            boolean resetLockoutRequiresChallenge) {
+        final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+        if (prop.commonProps.componentInfo != null) {
+            for (ComponentInfo info : prop.commonProps.componentInfo) {
+                componentInfo.add(new ComponentInfoInternal(info.componentId,
+                        info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                        info.softwareVersion));
+            }
+        }
+        final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+                prop.commonProps.sensorId, prop.commonProps.sensorStrength,
+                prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType,
+                prop.supportsDetectInteraction, prop.halControlsPreview,
+                resetLockoutRequiresChallenge);
+
+        return internalProp;
     }
 
-    @NonNull Supplier<AidlSession> getLazySession() {
+    @NonNull public Supplier<AidlSession> getLazySession() {
         return mLazySession;
     }
 
-    @NonNull FaceSensorPropertiesInternal getSensorProperties() {
+    @NonNull protected FaceSensorPropertiesInternal getSensorProperties() {
         return mSensorProperties;
     }
 
-    @VisibleForTesting @Nullable AidlSession getSessionForUser(int userId) {
+    @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) {
         if (mCurrentSession != null && mCurrentSession.getUserId() == userId) {
             return mCurrentSession;
         } else {
@@ -174,15 +243,18 @@
                 mProvider, this);
     }
 
-    @NonNull BiometricScheduler getScheduler() {
+    @NonNull public BiometricScheduler getScheduler() {
         return mScheduler;
     }
 
-    @NonNull LockoutCache getLockoutCache() {
-        return mLockoutCache;
+    @NonNull protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        if (forAuth) {
+            return null;
+        }
+        return mLockoutTracker;
     }
 
-    @NonNull Map<Integer, Long> getAuthenticatorIds() {
+    @NonNull protected Map<Integer, Long> getAuthenticatorIds() {
         return mAuthenticatorIds;
     }
 
@@ -253,4 +325,49 @@
         mScheduler.reset();
         mCurrentSession = null;
     }
+
+    protected BiometricContext getBiometricContext() {
+        return mBiometricContext;
+    }
+
+    protected Handler getHandler() {
+        return mHandler;
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Schedules FaceUpdateActiveUserClient for user id.
+     */
+    public void scheduleFaceUpdateActiveUserClient(int userId) {}
+
+    /**
+     * Returns true if the sensor hardware is detected.
+     */
+    public boolean isHardwareDetected(String halInstanceName) {
+        if (mTestHalEnabled) {
+            return true;
+        }
+        return ServiceManager.checkService(IFace.DESCRIPTOR + "/" + halInstanceName) != null;
+    }
+
+    /**
+     * Returns lockout mode of this sensor.
+     */
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                Utils.getCurrentStrength(mSensorProperties.sensorId));
+    }
+
+    public void setScheduler(BiometricScheduler scheduler) {
+        mScheduler = scheduler;
+    }
+
+    public void setLazySession(
+            Supplier<AidlSession> lazySession) {
+        mLazySession = lazySession;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java
index 8385c3f..0c34d70 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceUpdateActiveUserClient.java
@@ -27,13 +27,14 @@
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
-import com.android.server.biometrics.sensors.HalClientMonitor;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.face.aidl.AidlSession;
 
 import java.io.File;
 import java.util.Map;
 import java.util.function.Supplier;
 
-public class FaceUpdateActiveUserClient extends HalClientMonitor<IBiometricsFace> {
+public class FaceUpdateActiveUserClient extends StartUserClient<IBiometricsFace, AidlSession> {
     private static final String TAG = "FaceUpdateActiveUserClient";
     private static final String FACE_DATA_DIR = "facedata";
 
@@ -45,8 +46,18 @@
             int sensorId, @NonNull BiometricLogger logger,
             @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics,
             @NonNull Map<Integer, Long> authenticatorIds) {
-        super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
-                0 /* cookie */, sensorId, logger, biometricContext);
+        this(context, lazyDaemon, (newUserId, newUser, halInterfaceVersion) -> {},
+                userId, owner, sensorId, logger, biometricContext, hasEnrolledBiometrics,
+                authenticatorIds);
+    }
+
+    FaceUpdateActiveUserClient(@NonNull Context context,
+            @NonNull Supplier<IBiometricsFace> lazyDaemon, UserStartedCallback userStartedCallback,
+            int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger logger,
+            @NonNull BiometricContext biometricContext, boolean hasEnrolledBiometrics,
+            @NonNull Map<Integer, Long> authenticatorIds) {
+        super(context, lazyDaemon, null /* token */, userId, sensorId, logger, biometricContext,
+                userStartedCallback);
         mHasEnrolledBiometrics = hasEnrolledBiometrics;
         mAuthenticatorIds = authenticatorIds;
     }
@@ -77,6 +88,7 @@
             daemon.setActiveUser(getTargetUserId(), storePath.getAbsolutePath());
             mAuthenticatorIds.put(getTargetUserId(),
                     mHasEnrolledBiometrics ? daemon.getAuthenticatorId().value : 0L);
+            mUserStartedCallback.onUserStarted(getTargetUserId(), null, 0);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to setActiveUser: " + e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java
index 36a9790..7a574ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlCallbackConverter.java
@@ -112,4 +112,8 @@
     void onAuthenticatorIdRetrieved(long authenticatorId) {
         mAidlResponseHandler.onAuthenticatorIdRetrieved(authenticatorId);
     }
+
+    void onUnsupportedClientScheduled() {
+        mAidlResponseHandler.onUnsupportedClientScheduled();
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java
new file mode 100644
index 0000000..6355cb5
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * 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.biometrics.sensors.face.hidl;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.hardware.biometrics.face.SensorProps;
+import android.hardware.biometrics.face.V1_0.IBiometricsFace;
+import android.os.Handler;
+import android.os.IHwBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.face.FaceUtils;
+import com.android.server.biometrics.sensors.face.LockoutHalImpl;
+import com.android.server.biometrics.sensors.face.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.face.aidl.AidlSession;
+import com.android.server.biometrics.sensors.face.aidl.FaceProvider;
+import com.android.server.biometrics.sensors.face.aidl.Sensor;
+
+/**
+ * Convert HIDL sensor configurations to an AIDL Sensor.
+ */
+public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRecipient{
+
+    private static final String TAG = "HidlToAidlSensorAdapter";
+
+    private IBiometricsFace mDaemon;
+    private AidlSession mSession;
+    private int mCurrentUserId = UserHandle.USER_NULL;
+    private final Runnable mInternalCleanupAndGetFeatureRunnable;
+    private final FaceProvider mFaceProvider;
+    private final LockoutResetDispatcher mLockoutResetDispatcher;
+    private final AuthSessionCoordinator mAuthSessionCoordinator;
+    private final AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    private final StartUserClient.UserStartedCallback<AidlSession> mUserStartedCallback =
+            (newUserId, newUser, halInterfaceVersion) -> {
+                if (newUserId != mCurrentUserId) {
+                    handleUserChanged(newUserId);
+                }
+            };
+    private LockoutHalImpl mLockoutTracker;
+
+    public HidlToAidlSensorAdapter(@NonNull String tag,
+            @NonNull FaceProvider provider,
+            @NonNull Context context,
+            @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge,
+            @NonNull Runnable internalCleanupAndGetFeatureRunnable) {
+        this(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
+                resetLockoutRequiresChallenge, internalCleanupAndGetFeatureRunnable,
+                new AuthSessionCoordinator(), null /* daemon */,
+                null /* onEnrollSuccessCallback */);
+    }
+
+    @VisibleForTesting
+    HidlToAidlSensorAdapter(@NonNull String tag,
+            @NonNull FaceProvider provider,
+            @NonNull Context context,
+            @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresChallenge,
+            @NonNull Runnable internalCleanupAndGetFeatureRunnable,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @Nullable IBiometricsFace daemon,
+            @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) {
+        super(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext,
+                resetLockoutRequiresChallenge);
+        mInternalCleanupAndGetFeatureRunnable = internalCleanupAndGetFeatureRunnable;
+        mFaceProvider = provider;
+        mLockoutResetDispatcher = lockoutResetDispatcher;
+        mAuthSessionCoordinator = authSessionCoordinator;
+        mDaemon = daemon;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback == null
+                ? new AidlResponseHandler.AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {
+                        scheduleFaceUpdateActiveUserClient(mCurrentUserId);
+                    }
+
+                    @Override
+                    public void onHardwareUnavailable() {
+                        mDaemon = null;
+                        mCurrentUserId = UserHandle.USER_NULL;
+                    }
+                } : aidlResponseHandlerCallback;
+    }
+
+    @Override
+    public void scheduleFaceUpdateActiveUserClient(int userId) {
+        getScheduler().scheduleClientMonitor(getFaceUpdateActiveUserClient(userId));
+    }
+
+    @Override
+    public void serviceDied(long cookie) {
+        Slog.d(TAG, "HAL died.");
+        mDaemon = null;
+    }
+
+    @Override
+    public boolean isHardwareDetected(String halInstanceName) {
+        return getIBiometricsFace() != null;
+    }
+
+    @Override
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mLockoutTracker.getLockoutModeForUser(userId);
+    }
+
+    @Override
+    public void init(LockoutResetDispatcher lockoutResetDispatcher,
+            FaceProvider provider) {
+        setScheduler(new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+                null /* gestureAvailabilityTracker */));
+        setLazySession(this::getSession);
+        mLockoutTracker = new LockoutHalImpl();
+    }
+
+    @Override
+    @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) {
+        if (mSession != null && mSession.getUserId() == userId) {
+            return mSession;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        return mLockoutTracker;
+    }
+
+    @NonNull AidlSession getSession() {
+        if (mDaemon != null && mSession != null) {
+            return mSession;
+        } else {
+            return mSession = new AidlSession(getContext(), this::getIBiometricsFace,
+                    mCurrentUserId, getAidlResponseHandler());
+        }
+    }
+
+    private AidlResponseHandler getAidlResponseHandler() {
+        return new AidlResponseHandler(getContext(), getScheduler(), getSensorProperties().sensorId,
+                mCurrentUserId, mLockoutTracker, mLockoutResetDispatcher,
+                mAuthSessionCoordinator, () -> {}, mAidlResponseHandlerCallback);
+    }
+
+    private IBiometricsFace getIBiometricsFace() {
+        if (mFaceProvider.getTestHalEnabled()) {
+            final TestHal testHal = new TestHal(getContext(), getSensorProperties().sensorId);
+            testHal.setCallback(new HidlToAidlCallbackConverter(getAidlResponseHandler()));
+            return testHal;
+        }
+
+        if (mDaemon != null) {
+            return mDaemon;
+        }
+
+        Slog.d(TAG, "Daemon was null, reconnecting, current operation: "
+                + getScheduler().getCurrentClient());
+
+        try {
+            mDaemon = IBiometricsFace.getService();
+        } catch (java.util.NoSuchElementException e) {
+            // Service doesn't exist or cannot be opened.
+            Slog.w(TAG, "NoSuchElementException", e);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get face HAL", e);
+        }
+
+        if (mDaemon == null) {
+            Slog.w(TAG, "Face HAL not available");
+            return null;
+        }
+
+        mDaemon.asBinder().linkToDeath(this, 0 /* flags */);
+
+        scheduleLoadAuthenticatorIds();
+        mInternalCleanupAndGetFeatureRunnable.run();
+        return mDaemon;
+    }
+
+    @VisibleForTesting void handleUserChanged(int newUserId) {
+        Slog.d(TAG, "User changed. Current user is " + newUserId);
+        mSession = null;
+        mCurrentUserId = newUserId;
+    }
+
+    private void scheduleLoadAuthenticatorIds() {
+        // Note that this can be performed on the scheduler (as opposed to being done immediately
+        // when the HAL is (re)loaded, since
+        // 1) If this is truly the first time it's being performed (e.g. system has just started),
+        //    this will be run very early and way before any applications need to generate keys.
+        // 2) If this is being performed to refresh the authenticatorIds (e.g. HAL crashed and has
+        //    just been reloaded), the framework already has a cache of the authenticatorIds. This
+        //    is safe because authenticatorIds only change when A) new template has been enrolled,
+        //    or B) all templates are removed.
+        getHandler().post(() -> {
+            for (UserInfo user : UserManager.get(getContext()).getAliveUsers()) {
+                final int targetUserId = user.id;
+                if (!getAuthenticatorIds().containsKey(targetUserId)) {
+                    scheduleFaceUpdateActiveUserClient(targetUserId);
+                }
+            }
+        });
+    }
+
+    private FaceUpdateActiveUserClient getFaceUpdateActiveUserClient(int userId) {
+        return new FaceUpdateActiveUserClient(getContext(), this::getIBiometricsFace,
+                mUserStartedCallback, userId, TAG, getSensorProperties().sensorId,
+                BiometricLogger.ofUnknown(getContext()), getBiometricContext(),
+                !FaceUtils.getInstance(getSensorProperties().sensorId).getBiometricsForUser(
+                        getContext(), userId).isEmpty(),
+                getAuthenticatorIds());
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
similarity index 88%
rename from services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java
rename to services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
index 489b213..5daf2d4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java
@@ -47,34 +47,37 @@
 import java.util.function.Supplier;
 
 /**
- * Adapter to convert AIDL-specific interface {@link ISession} methods to HIDL implementation.
+ * Adapter to convert HIDL methods into AIDL interface {@link ISession}.
  */
-public class AidlToHidlAdapter implements ISession {
+public class HidlToAidlSessionAdapter implements ISession {
 
-    private final String TAG = "AidlToHidlAdapter";
+    private static final String TAG = "HidlToAidlSessionAdapter";
+
     private static final int CHALLENGE_TIMEOUT_SEC = 600;
     @DurationMillisLong
     private static final int GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS = 60 * 1000;
     @DurationMillisLong
     private static final int GENERATE_CHALLENGE_COUNTER_TTL_MILLIS = CHALLENGE_TIMEOUT_SEC * 1000;
     private static final int INVALID_VALUE = -1;
+    @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 75;
+
     private final Clock mClock;
     private final List<Long> mGeneratedChallengeCount = new ArrayList<>();
-    @VisibleForTesting static final int ENROLL_TIMEOUT_SEC = 75;
+    private final int mUserId;
+    private final Context mContext;
+
     private long mGenerateChallengeCreatedAt = INVALID_VALUE;
     private long mGenerateChallengeResult = INVALID_VALUE;
     @NonNull private Supplier<IBiometricsFace> mSession;
-    private final int mUserId;
     private HidlToAidlCallbackConverter mHidlToAidlCallbackConverter;
-    private final Context mContext;
     private int mFeature = INVALID_VALUE;
 
-    public AidlToHidlAdapter(Context context, Supplier<IBiometricsFace> session,
+    public HidlToAidlSessionAdapter(Context context, Supplier<IBiometricsFace> session,
             int userId, AidlResponseHandler aidlResponseHandler) {
         this(context, session, userId, aidlResponseHandler, Clock.systemUTC());
     }
 
-    AidlToHidlAdapter(Context context, Supplier<IBiometricsFace> session, int userId,
+    HidlToAidlSessionAdapter(Context context, Supplier<IBiometricsFace> session, int userId,
             AidlResponseHandler aidlResponseHandler, Clock clock) {
         mSession = session;
         mUserId = userId;
@@ -83,42 +86,11 @@
         setCallback(aidlResponseHandler);
     }
 
-    private void setCallback(AidlResponseHandler aidlResponseHandler) {
-        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
-        try {
-            mSession.get().setCallback(mHidlToAidlCallbackConverter);
-        } catch (RemoteException e) {
-            Slog.d(TAG, "Failed to set callback");
-        }
-    }
-
     @Override
     public IBinder asBinder() {
         return null;
     }
 
-    private boolean isGeneratedChallengeCacheValid() {
-        return mGenerateChallengeCreatedAt != INVALID_VALUE
-                && mGenerateChallengeResult != INVALID_VALUE
-                && mClock.millis() - mGenerateChallengeCreatedAt
-                < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS;
-    }
-
-    private void incrementChallengeCount() {
-        mGeneratedChallengeCount.add(0, mClock.millis());
-    }
-
-    private int decrementChallengeCount() {
-        final long now = mClock.millis();
-        // ignore values that are old in case generate/revoke calls are not matched
-        // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing
-        mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS);
-        if (!mGeneratedChallengeCount.isEmpty()) {
-            mGeneratedChallengeCount.remove(0);
-        }
-        return mGeneratedChallengeCount.size();
-    }
-
     @Override
     public void generateChallenge() throws RemoteException {
         incrementChallengeCount();
@@ -150,7 +122,7 @@
 
     @Override
     public EnrollmentStageConfig[] getEnrollmentConfig(byte enrollmentType) throws RemoteException {
-        //unsupported in HIDL
+        Slog.e(TAG, "getEnrollmentConfig unsupported in HIDL");
         return null;
     }
 
@@ -244,19 +216,6 @@
         }
     }
 
-    private int getFaceId() {
-        FaceManager faceManager = mContext.getSystemService(FaceManager.class);
-        List<Face> faces = faceManager.getEnrolledFaces(mUserId);
-        if (faces.isEmpty()) {
-            Slog.d(TAG, "No faces to get feature from.");
-            mHidlToAidlCallbackConverter.onError(0 /* deviceId */, mUserId,
-                    BiometricFaceConstants.FACE_ERROR_NOT_ENROLLED, 0 /* vendorCode */);
-            return INVALID_VALUE;
-        }
-
-        return faces.get(0).getBiometricId();
-    }
-
     @Override
     public void getAuthenticatorId() throws RemoteException {
         long authenticatorId = mSession.get().getAuthenticatorId().value;
@@ -265,7 +224,8 @@
 
     @Override
     public void invalidateAuthenticatorId() throws RemoteException {
-        //unsupported in HIDL
+        Slog.e(TAG, "invalidateAuthenticatorId unsupported in HIDL");
+        mHidlToAidlCallbackConverter.onUnsupportedClientScheduled();
     }
 
     @Override
@@ -279,47 +239,105 @@
 
     @Override
     public void close() throws RemoteException {
-        //Unsupported in HIDL
+        Slog.e(TAG, "close unsupported in HIDL");
     }
 
     @Override
     public ICancellationSignal authenticateWithContext(long operationId, OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Slog.e(TAG, "authenticateWithContext unsupported in HIDL");
+        return authenticate(operationId);
     }
 
     @Override
     public ICancellationSignal enrollWithContext(HardwareAuthToken hat, byte type, byte[] features,
             NativeHandle previewSurface, OperationContext context) throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Slog.e(TAG, "enrollWithContext unsupported in HIDL");
+        return enroll(hat, type, features, previewSurface);
     }
 
     @Override
     public ICancellationSignal detectInteractionWithContext(OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Slog.e(TAG, "detectInteractionWithContext unsupported in HIDL");
+        return detectInteraction();
     }
 
     @Override
     public void onContextChanged(OperationContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Slog.e(TAG, "onContextChanged unsupported in HIDL");
     }
 
     @Override
     public int getInterfaceVersion() throws RemoteException {
-        //Unsupported in HIDL
+        Slog.e(TAG, "getInterfaceVersion unsupported in HIDL");
         return 0;
     }
 
     @Override
     public String getInterfaceHash() throws RemoteException {
+        Slog.e(TAG, "getInterfaceHash unsupported in HIDL");
+        return null;
+    }
+
+    @Override
+    public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) {
         //Unsupported in HIDL
         return null;
     }
 
+    private boolean isGeneratedChallengeCacheValid() {
+        return mGenerateChallengeCreatedAt != INVALID_VALUE
+                && mGenerateChallengeResult != INVALID_VALUE
+                && mClock.millis() - mGenerateChallengeCreatedAt
+                < GENERATE_CHALLENGE_REUSE_INTERVAL_MILLIS;
+    }
+
+    private void incrementChallengeCount() {
+        mGeneratedChallengeCount.add(0, mClock.millis());
+    }
+
+    private int decrementChallengeCount() {
+        final long now = mClock.millis();
+        // ignore values that are old in case generate/revoke calls are not matched
+        // this doesn't ensure revoke if calls are mismatched but it keeps the list from growing
+        mGeneratedChallengeCount.removeIf(x -> now - x > GENERATE_CHALLENGE_COUNTER_TTL_MILLIS);
+        if (!mGeneratedChallengeCount.isEmpty()) {
+            mGeneratedChallengeCount.remove(0);
+        }
+        return mGeneratedChallengeCount.size();
+    }
+
+    private void setCallback(AidlResponseHandler aidlResponseHandler) {
+        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
+        try {
+            if (mSession.get() != null) {
+                long halId = mSession.get().setCallback(mHidlToAidlCallbackConverter).value;
+                Slog.d(TAG, "Face HAL ready, HAL ID: " + halId);
+                if (halId == 0) {
+                    Slog.d(TAG, "Unable to set HIDL callback.");
+                }
+            } else {
+                Slog.e(TAG, "Unable to set HIDL callback. HIDL daemon is null.");
+            }
+        } catch (RemoteException e) {
+            Slog.d(TAG, "Failed to set callback");
+        }
+    }
+
+    private int getFaceId() {
+        FaceManager faceManager = mContext.getSystemService(FaceManager.class);
+        List<Face> faces = faceManager.getEnrolledFaces(mUserId);
+        if (faces.isEmpty()) {
+            Slog.d(TAG, "No faces to get feature from.");
+            mHidlToAidlCallbackConverter.onError(0 /* deviceId */, mUserId,
+                    BiometricFaceConstants.FACE_ERROR_NOT_ENROLLED, 0 /* vendorCode */);
+            return INVALID_VALUE;
+        }
+
+        return faces.get(0).getBiometricId();
+    }
+
     /**
      * Cancellation in HIDL occurs for the entire session, instead of a specific client.
      */
@@ -345,10 +363,4 @@
             return null;
         }
     }
-
-    @Override
-    public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) {
-        //Unsupported in HIDL
-        return null;
-    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 83b306b..e01d672 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -45,9 +45,11 @@
 import android.hardware.biometrics.ITestSessionCallback;
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.PointerContext;
+import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintServiceReceiver;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
@@ -80,6 +82,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.AuthenticationStateListeners;
@@ -127,6 +130,8 @@
     @NonNull
     private final Function<String, FingerprintProvider> mFingerprintProvider;
     @NonNull
+    private final FingerprintProviderFunction mFingerprintProviderFunction;
+    @NonNull
     private final BiometricStateCallback<ServiceProvider, FingerprintSensorPropertiesInternal>
             mBiometricStateCallback;
     @NonNull
@@ -136,6 +141,11 @@
     @NonNull
     private final FingerprintServiceRegistry mRegistry;
 
+    interface FingerprintProviderFunction {
+        FingerprintProvider getFingerprintProvider(Pair<String, SensorProps[]> filteredSensorProp,
+                boolean resetLockoutRequiresHardwareAuthToken);
+    }
+
     /** Receives the incoming binder calls from FingerprintManager. */
     @VisibleForTesting
     final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() {
@@ -874,6 +884,18 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
+        public void registerAuthenticatorsLegacy(
+                @NonNull FingerprintSensorConfigurations fingerprintSensorConfigurations) {
+            super.registerAuthenticatorsLegacy_enforcePermission();
+            if (!fingerprintSensorConfigurations.hasSensorConfigurations()) {
+                Slog.d(TAG, "No fingerprint sensors available.");
+                return;
+            }
+            mRegistry.registerAll(() -> getProviders(fingerprintSensorConfigurations));
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
             super.registerAuthenticators_enforcePermission();
@@ -1021,7 +1043,8 @@
                 () -> IBiometricService.Stub.asInterface(
                         ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
                 () -> ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR),
-                null /* fingerprintProvider */);
+                null /* fingerprintProvider */,
+                null /* fingerprintProviderFunction */);
     }
 
     @VisibleForTesting
@@ -1029,7 +1052,8 @@
             BiometricContext biometricContext,
             Supplier<IBiometricService> biometricServiceSupplier,
             Supplier<String[]> aidlInstanceNameSupplier,
-            Function<String, FingerprintProvider> fingerprintProvider) {
+            Function<String, FingerprintProvider> fingerprintProvider,
+            FingerprintProviderFunction fingerprintProviderFunction) {
         super(context);
         mBiometricContext = biometricContext;
         mAidlInstanceNameSupplier = aidlInstanceNameSupplier;
@@ -1049,7 +1073,8 @@
                             return new FingerprintProvider(getContext(),
                                     mBiometricStateCallback, mAuthenticationStateListeners,
                                     fp.getSensorProps(), name, mLockoutResetDispatcher,
-                                    mGestureAvailabilityDispatcher, mBiometricContext);
+                                    mGestureAvailabilityDispatcher, mBiometricContext,
+                                    true /* resetLockoutRequiresHardwareAuthToken */);
                         } catch (RemoteException e) {
                             Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
                         }
@@ -1059,6 +1084,22 @@
 
                     return null;
                 };
+        if (Flags.deHidl()) {
+            mFingerprintProviderFunction = fingerprintProviderFunction == null
+                    ? (filteredSensorProps, resetLockoutRequiresHardwareAuthToken) ->
+                            new FingerprintProvider(
+                                    getContext(), mBiometricStateCallback,
+                                    mAuthenticationStateListeners,
+                                    filteredSensorProps.second,
+                                    filteredSensorProps.first, mLockoutResetDispatcher,
+                                    mGestureAvailabilityDispatcher,
+                                    mBiometricContext,
+                                    resetLockoutRequiresHardwareAuthToken)
+                    : fingerprintProviderFunction;
+        } else {
+            mFingerprintProviderFunction =
+                    (filteredSensorProps, resetLockoutRequiresHardwareAuthToken) -> null;
+        }
         mHandler = new Handler(Looper.getMainLooper());
         mRegistry = new FingerprintServiceRegistry(mServiceWrapper, biometricServiceSupplier);
         mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -1070,6 +1111,44 @@
         });
     }
 
+    @NonNull
+    private List<ServiceProvider> getProviders(@NonNull FingerprintSensorConfigurations
+            fingerprintSensorConfigurations) {
+        final List<ServiceProvider> providers = new ArrayList<>();
+        final Pair<String, SensorProps[]> filteredSensorProps = filterAvailableHalInstances(
+                fingerprintSensorConfigurations);
+        providers.add(mFingerprintProviderFunction.getFingerprintProvider(filteredSensorProps,
+                fingerprintSensorConfigurations.getResetLockoutRequiresHardwareAuthToken()));
+
+        return providers;
+    }
+
+    @NonNull
+    private Pair<String, SensorProps[]> filterAvailableHalInstances(
+            FingerprintSensorConfigurations fingerprintSensorConfigurations) {
+        Pair<String, SensorProps[]> finalSensorPair =
+                fingerprintSensorConfigurations.getSensorPair();
+        if (fingerprintSensorConfigurations.isSingleSensorConfigurationPresent()) {
+            return finalSensorPair;
+        }
+
+        final Pair<String, SensorProps[]> virtualSensorPropsPair = fingerprintSensorConfigurations
+                .getSensorPairForInstance("virtual");
+        if (Utils.isVirtualEnabled(getContext())) {
+            if (virtualSensorPropsPair != null) {
+                return virtualSensorPropsPair;
+            } else {
+                Slog.e(TAG, "Could not find virtual interface while it is enabled");
+                return finalSensorPair;
+            }
+        } else {
+            if (virtualSensorPropsPair != null) {
+                return fingerprintSensorConfigurations.getSensorPairNotForInstance("virtual");
+            }
+        }
+        return finalSensorPair;
+    }
+
     private Pair<List<FingerprintSensorPropertiesInternal>, List<String>>
                 filterAvailableHalInstances(
             @NonNull List<FingerprintSensorPropertiesInternal> hidlInstances,
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java
index 4a01943..bd21cf4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java
@@ -25,6 +25,7 @@
 import android.hardware.keymaster.HardwareAuthToken;
 import android.util.Slog;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.AcquisitionClient;
@@ -34,9 +35,9 @@
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.EnumerateConsumer;
 import com.android.server.biometrics.sensors.ErrorConsumer;
-import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutConsumer;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.RemovalConsumer;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
 
@@ -59,6 +60,21 @@
         void onHardwareUnavailable();
     }
 
+    /**
+     * Interface to send results to the AidlResponseHandler's owner.
+     */
+    public interface AidlResponseHandlerCallback {
+        /**
+         * Invoked when enrollment is successful.
+         */
+        void onEnrollSuccess();
+
+        /**
+         * Invoked when the HAL sends ERROR_HW_UNAVAILABLE.
+         */
+        void onHardwareUnavailable();
+    }
+
     private static final String TAG = "AidlResponseHandler";
 
     @NonNull
@@ -68,28 +84,49 @@
     private final int mSensorId;
     private final int mUserId;
     @NonNull
-    private final LockoutCache mLockoutCache;
+    private final LockoutTracker mLockoutTracker;
     @NonNull
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     @NonNull
     private final AuthSessionCoordinator mAuthSessionCoordinator;
     @NonNull
     private final HardwareUnavailableCallback mHardwareUnavailableCallback;
+    @NonNull
+    private final AidlResponseHandlerCallback mAidlResponseHandlerCallback;
 
     public AidlResponseHandler(@NonNull Context context,
             @NonNull BiometricScheduler scheduler, int sensorId, int userId,
-            @NonNull LockoutCache lockoutTracker,
+            @NonNull LockoutTracker lockoutTracker,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull AuthSessionCoordinator authSessionCoordinator,
             @NonNull HardwareUnavailableCallback hardwareUnavailableCallback) {
+        this(context, scheduler, sensorId, userId, lockoutTracker, lockoutResetDispatcher,
+                authSessionCoordinator, hardwareUnavailableCallback,
+                new AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {}
+
+                    @Override
+                    public void onHardwareUnavailable() {}
+                });
+    }
+
+    public AidlResponseHandler(@NonNull Context context,
+            @NonNull BiometricScheduler scheduler, int sensorId, int userId,
+            @NonNull LockoutTracker lockoutTracker,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @NonNull HardwareUnavailableCallback hardwareUnavailableCallback,
+            @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) {
         mContext = context;
         mScheduler = scheduler;
         mSensorId = sensorId;
         mUserId = userId;
-        mLockoutCache = lockoutTracker;
+        mLockoutTracker = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mAuthSessionCoordinator = authSessionCoordinator;
         mHardwareUnavailableCallback = hardwareUnavailableCallback;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback;
     }
 
     @Override
@@ -105,27 +142,26 @@
     @Override
     public void onChallengeGenerated(long challenge) {
         handleResponse(FingerprintGenerateChallengeClient.class, (c) -> c.onChallengeGenerated(
-                mSensorId, mUserId, challenge), null);
+                mSensorId, mUserId, challenge));
     }
 
     @Override
     public void onChallengeRevoked(long challenge) {
         handleResponse(FingerprintRevokeChallengeClient.class, (c) -> c.onChallengeRevoked(
-                challenge), null);
+                challenge));
     }
 
     /**
      * Handles acquired messages sent by the HAL (specifically for HIDL HAL).
      */
     public void onAcquired(int acquiredInfo, int vendorCode) {
-        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode),
-                null);
+        handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(acquiredInfo, vendorCode));
     }
 
     @Override
     public void onAcquired(byte info, int vendorCode) {
         handleResponse(AcquisitionClient.class, (c) -> c.onAcquired(
-                AidlConversionUtils.toFrameworkAcquiredInfo(info), vendorCode), null);
+                AidlConversionUtils.toFrameworkAcquiredInfo(info), vendorCode));
     }
 
     /**
@@ -135,9 +171,13 @@
         handleResponse(ErrorConsumer.class, (c) -> {
             c.onError(error, vendorCode);
             if (error == Error.HW_UNAVAILABLE) {
-                mHardwareUnavailableCallback.onHardwareUnavailable();
+                if (Flags.deHidl()) {
+                    mAidlResponseHandlerCallback.onHardwareUnavailable();
+                } else {
+                    mHardwareUnavailableCallback.onHardwareUnavailable();
+                }
             }
-        }, null);
+        });
     }
 
     @Override
@@ -158,8 +198,12 @@
                 .getUniqueName(mContext, currentUserId);
         final Fingerprint fingerprint = new Fingerprint(name, currentUserId,
                 enrollmentId, mSensorId);
-        handleResponse(FingerprintEnrollClient.class, (c) -> c.onEnrollResult(fingerprint,
-                remaining), null);
+        handleResponse(FingerprintEnrollClient.class, (c) -> {
+            c.onEnrollResult(fingerprint, remaining);
+            if (remaining == 0) {
+                mAidlResponseHandlerCallback.onEnrollSuccess();
+            }
+        });
     }
 
     @Override
@@ -184,13 +228,12 @@
 
     @Override
     public void onLockoutTimed(long durationMillis) {
-        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis),
-                null);
+        handleResponse(LockoutConsumer.class, (c) -> c.onLockoutTimed(durationMillis));
     }
 
     @Override
     public void onLockoutPermanent() {
-        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent, null);
+        handleResponse(LockoutConsumer.class, LockoutConsumer::onLockoutPermanent);
     }
 
     @Override
@@ -198,7 +241,7 @@
         handleResponse(FingerprintResetLockoutClient.class,
                 FingerprintResetLockoutClient::onLockoutCleared,
                 (c) -> FingerprintResetLockoutClient.resetLocalLockoutStateToNone(
-                        mSensorId, mUserId, mLockoutCache, mLockoutResetDispatcher,
+                        mSensorId, mUserId, mLockoutTracker, mLockoutResetDispatcher,
                         mAuthSessionCoordinator, Utils.getCurrentStrength(mSensorId),
                         -1 /* requestId */));
     }
@@ -206,49 +249,74 @@
     @Override
     public void onInteractionDetected() {
         handleResponse(FingerprintDetectClient.class,
-                FingerprintDetectClient::onInteractionDetected, null);
+                FingerprintDetectClient::onInteractionDetected);
     }
 
     @Override
     public void onEnrollmentsEnumerated(int[] enrollmentIds) {
         if (enrollmentIds.length > 0) {
             for (int i = 0; i < enrollmentIds.length; i++) {
-                final Fingerprint fp = new Fingerprint("", enrollmentIds[i], mSensorId);
-                int finalI = i;
-                handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(fp,
-                        enrollmentIds.length - finalI - 1), null);
+                onEnrollmentEnumerated(enrollmentIds[i],
+                        enrollmentIds.length - i - 1 /* remaining */);
             }
         } else {
-            handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(null,
-                    0), null);
+            handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(
+                    null /* identifier */,
+                    0 /* remaining */));
         }
     }
 
+    /**
+     * Handle enumerated fingerprint.
+     */
+    public void onEnrollmentEnumerated(int enrollmentId, int remaining) {
+        final Fingerprint fp = new Fingerprint("", enrollmentId, mSensorId);
+        handleResponse(EnumerateConsumer.class, (c) -> c.onEnumerationResult(fp, remaining));
+    }
+
+    /**
+     * Handle removal of fingerprint.
+     */
+    public void onEnrollmentRemoved(int enrollmentId, int remaining) {
+        final Fingerprint fp = new Fingerprint("", enrollmentId, mSensorId);
+        handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(fp, remaining));
+    }
+
     @Override
     public void onEnrollmentsRemoved(int[] enrollmentIds) {
         if (enrollmentIds.length > 0) {
             for (int i  = 0; i < enrollmentIds.length; i++) {
-                final Fingerprint fp = new Fingerprint("", enrollmentIds[i], mSensorId);
-                int finalI = i;
-                handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(fp,
-                        enrollmentIds.length - finalI - 1), null);
+                onEnrollmentRemoved(enrollmentIds[i], enrollmentIds.length - i - 1 /* remaining */);
             }
         } else {
-            handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null, 0),
-                    null);
+            handleResponse(RemovalConsumer.class, (c) -> c.onRemoved(null /* identifier */,
+                    0 /* remaining */));
         }
     }
 
     @Override
     public void onAuthenticatorIdRetrieved(long authenticatorId) {
         handleResponse(FingerprintGetAuthenticatorIdClient.class,
-                (c) -> c.onAuthenticatorIdRetrieved(authenticatorId), null);
+                (c) -> c.onAuthenticatorIdRetrieved(authenticatorId));
     }
 
     @Override
     public void onAuthenticatorIdInvalidated(long newAuthenticatorId) {
         handleResponse(FingerprintInvalidationClient.class, (c) -> c.onAuthenticatorIdInvalidated(
-                newAuthenticatorId), null);
+                newAuthenticatorId));
+    }
+
+    /**
+     * Handle clients which are not supported in HIDL HAL.
+     */
+    public <T extends BaseClientMonitor> void onUnsupportedClientScheduled(Class<T> className) {
+        Slog.e(TAG, className + " is not supported in the HAL.");
+        handleResponse(className, (c) -> c.cancel());
+    }
+
+    private <T> void handleResponse(@NonNull Class<T> className,
+            @NonNull Consumer<T> action) {
+        handleResponse(className, action, null /* alternateAction */);
     }
 
     private <T> void handleResponse(@NonNull Class<T> className,
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java
index 299a310..8ff105b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java
@@ -20,7 +20,7 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 
-import com.android.server.biometrics.sensors.fingerprint.hidl.AidlToHidlAdapter;
+import com.android.server.biometrics.sensors.fingerprint.hidl.HidlToAidlSessionAdapter;
 
 import java.util.function.Supplier;
 
@@ -45,7 +45,7 @@
 
     public AidlSession(@NonNull Supplier<IBiometricsFingerprint> session,
             int userId, AidlResponseHandler aidlResponseHandler) {
-        mSession = new AidlToHidlAdapter(session, userId, aidlResponseHandler);
+        mSession = new HidlToAidlSessionAdapter(session, userId, aidlResponseHandler);
         mHalInterfaceVersion = 0;
         mUserId = userId;
         mAidlResponseHandler = aidlResponseHandler;
@@ -62,7 +62,7 @@
     }
 
     /** The HAL callback, which should only be used in tests {@See BiometricTestSessionImpl}. */
-    AidlResponseHandler getHalSessionCallback() {
+    public AidlResponseHandler getHalSessionCallback() {
         return mAidlResponseHandler;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java
index ea1a622..0353969 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java
@@ -30,7 +30,7 @@
 import java.util.Map;
 import java.util.function.Supplier;
 
-class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> {
+public class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> {
 
     private static final String TAG = "FingerprintGetAuthenticatorIdClient";
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 032ab87..88a11d9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -59,6 +59,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
 import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -73,6 +74,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.SensorList;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
@@ -80,6 +82,7 @@
 import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.ServiceProvider;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
+import com.android.server.biometrics.sensors.fingerprint.hidl.HidlToAidlSensorAdapter;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -165,10 +168,12 @@
             @NonNull SensorProps[] props, @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            @NonNull BiometricContext biometricContext) {
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresHardwareAuthToken) {
         this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
                 lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext,
-                null /* daemon */);
+                null /* daemon */, resetLockoutRequiresHardwareAuthToken,
+                false /* testHalEnabled */);
     }
 
     @VisibleForTesting FingerprintProvider(@NonNull Context context,
@@ -178,7 +183,9 @@
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext,
-            IFingerprint daemon) {
+            @Nullable IFingerprint daemon,
+            boolean resetLockoutRequiresHardwareAuthToken,
+            boolean testHalEnabled) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mAuthenticationStateListeners = authenticationStateListeners;
@@ -191,62 +198,136 @@
         mBiometricContext = biometricContext;
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
         mDaemon = daemon;
+        mTestHalEnabled = testHalEnabled;
 
-        AuthenticationStatsBroadcastReceiver mBroadcastReceiver =
-                new AuthenticationStatsBroadcastReceiver(
-                        mContext,
-                        BiometricsProtoEnums.MODALITY_FINGERPRINT,
-                        (AuthenticationStatsCollector collector) -> {
-                            Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
-                            mAuthenticationStatsCollector = collector;
-                        });
+        initAuthenticationBroadcastReceiver();
+        initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
+    }
 
-        final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context);
+    private void initAuthenticationBroadcastReceiver() {
+        new AuthenticationStatsBroadcastReceiver(
+                mContext,
+                BiometricsProtoEnums.MODALITY_FINGERPRINT,
+                (AuthenticationStatsCollector collector) -> {
+                    Slog.d(getTag(), "Initializing AuthenticationStatsCollector");
+                    mAuthenticationStatsCollector = collector;
+                });
+    }
 
-        for (SensorProps prop : props) {
-            final int sensorId = prop.commonProps.sensorId;
-
-            final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
-            if (prop.commonProps.componentInfo != null) {
-                for (ComponentInfo info : prop.commonProps.componentInfo) {
-                    componentInfo.add(new ComponentInfoInternal(info.componentId,
-                            info.hardwareVersion, info.firmwareVersion, info.serialNumber,
-                            info.softwareVersion));
+    private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props,
+            GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
+        if (Flags.deHidl()) {
+            if (!resetLockoutRequiresHardwareAuthToken) {
+                Slog.d(getTag(), "Adding HIDL configs");
+                for (SensorProps sensorConfig: props) {
+                    addHidlSensors(sensorConfig, gestureAvailabilityDispatcher,
+                            resetLockoutRequiresHardwareAuthToken);
+                }
+            } else {
+                Slog.d(getTag(), "Adding AIDL configs");
+                final List<SensorLocationInternal> workaroundLocations =
+                        getWorkaroundSensorProps(mContext);
+                for (SensorProps prop : props) {
+                    addAidlSensors(prop, gestureAvailabilityDispatcher, workaroundLocations,
+                            resetLockoutRequiresHardwareAuthToken);
                 }
             }
+        } else {
+            final List<SensorLocationInternal> workaroundLocations =
+                    getWorkaroundSensorProps(mContext);
 
-            final FingerprintSensorPropertiesInternal internalProp =
-                    new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId,
-                            prop.commonProps.sensorStrength,
-                            prop.commonProps.maxEnrollmentsPerUser,
-                            componentInfo,
-                            prop.sensorType,
-                            prop.halControlsIllumination,
-                            true /* resetLockoutRequiresHardwareAuthToken */,
-                            !workaroundLocations.isEmpty() ? workaroundLocations :
-                                    Arrays.stream(prop.sensorLocations).map(location ->
-                                                    new SensorLocationInternal(
-                                                            location.display,
-                                                            location.sensorLocationX,
-                                                            location.sensorLocationY,
-                                                            location.sensorRadius))
-                                            .collect(Collectors.toList()));
-            final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler,
-                    internalProp, lockoutResetDispatcher, gestureAvailabilityDispatcher,
-                    mBiometricContext);
-            final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
-                    sensor.getLazySession().get().getUserId();
-            mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
-                    new SynchronousUserSwitchObserver() {
-                        @Override
-                        public void onUserSwitching(int newUserId) {
-                            scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
-                        }
-                    });
-            Slog.d(getTag(), "Added: " + internalProp);
+            for (SensorProps prop : props) {
+                final int sensorId = prop.commonProps.sensorId;
+                final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+                if (prop.commonProps.componentInfo != null) {
+                    for (ComponentInfo info : prop.commonProps.componentInfo) {
+                        componentInfo.add(new ComponentInfoInternal(info.componentId,
+                                info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                                info.softwareVersion));
+                    }
+                }
+                final FingerprintSensorPropertiesInternal internalProp =
+                        new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId,
+                                prop.commonProps.sensorStrength,
+                                prop.commonProps.maxEnrollmentsPerUser,
+                                componentInfo,
+                                prop.sensorType,
+                                prop.halControlsIllumination,
+                                true /* resetLockoutRequiresHardwareAuthToken */,
+                                !workaroundLocations.isEmpty() ? workaroundLocations :
+                                        Arrays.stream(prop.sensorLocations).map(
+                                                        location -> new SensorLocationInternal(
+                                                                location.display,
+                                                                location.sensorLocationX,
+                                                                location.sensorLocationY,
+                                                                location.sensorRadius))
+                                                .collect(Collectors.toList()));
+                final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext,
+                        mHandler, internalProp, mLockoutResetDispatcher,
+                        gestureAvailabilityDispatcher, mBiometricContext);
+                sensor.init(gestureAvailabilityDispatcher,
+                        mLockoutResetDispatcher);
+                final int sessionUserId =
+                        sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                                sensor.getLazySession().get().getUserId();
+                mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
+                        new SynchronousUserSwitchObserver() {
+                            @Override
+                            public void onUserSwitching(int newUserId) {
+                                scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                            }
+                        });
+                Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString());
+            }
         }
     }
 
+    private void addHidlSensors(@NonNull SensorProps prop,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/"
+                + sensorId, this, mContext, mHandler,
+                prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher,
+                mBiometricContext, resetLockoutRequiresHardwareAuthToken,
+                () -> scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(),
+                        null /* callback */));
+        sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString());
+    }
+
+    private void addAidlSensors(@NonNull SensorProps prop,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            List<SensorLocationInternal> workaroundLocations,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        final int sensorId = prop.commonProps.sensorId;
+        final Sensor sensor = new Sensor(getTag() + "/" + sensorId,
+                this, mContext, mHandler,
+                prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher,
+                mBiometricContext, workaroundLocations,
+                resetLockoutRequiresHardwareAuthToken);
+        sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL :
+                sensor.getLazySession().get().getUserId();
+        mFingerprintSensors.addSensor(sensorId, sensor, sessionUserId,
+                new SynchronousUserSwitchObserver() {
+                    @Override
+                    public void onUserSwitching(int newUserId) {
+                        scheduleInternalCleanup(sensorId, newUserId, null /* callback */);
+                    }
+                });
+        Slog.d(getTag(), "Added: " + mFingerprintSensors.get(sensorId).toString());
+    }
+
     private String getTag() {
         return "FingerprintProvider/" + mHalInstanceName;
     }
@@ -351,7 +432,10 @@
         }
     }
 
-    private void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
+    /**
+     * Schedules FingerprintGetAuthenticatorIdClient for specific sensor and user.
+     */
+    protected void scheduleLoadAuthenticatorIdsForUser(int sensorId, int userId) {
         mHandler.post(() -> {
             final FingerprintGetAuthenticatorIdClient client =
                     new FingerprintGetAuthenticatorIdClient(mContext,
@@ -387,8 +471,8 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext, hardwareAuthToken,
-                    mFingerprintSensors.get(sensorId).getLockoutCache(), mLockoutResetDispatcher,
-                    Utils.getCurrentStrength(sensorId));
+                    mFingerprintSensors.get(sensorId).getLockoutTracker(false /* forAuth */),
+                    mLockoutResetDispatcher, Utils.getCurrentStrength(sensorId));
             scheduleForSensor(sensorId, client);
         });
     }
@@ -443,18 +527,23 @@
                     mFingerprintSensors.get(sensorId).getSensorProperties(),
                     mUdfpsOverlayController, mSidefpsController,
                     mAuthenticationStateListeners, maxTemplatesPerUser, enrollReason);
-            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
-                    mBiometricStateCallback, new ClientMonitorCallback() {
-                @Override
-                public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
-                        boolean success) {
-                    ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
-                    if (success) {
-                        scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
-                        scheduleInvalidationRequest(sensorId, userId);
-                    }
-                }
-            }));
+            if (Flags.deHidl()) {
+                scheduleForSensor(sensorId, client, mBiometricStateCallback);
+            } else {
+                scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+                        mBiometricStateCallback, new ClientMonitorCallback() {
+                            @Override
+                            public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+                                    boolean success) {
+                                ClientMonitorCallback.super.onClientFinished(
+                                        clientMonitor, success);
+                                if (success) {
+                                    scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
+                                    scheduleInvalidationRequest(sensorId, userId);
+                                }
+                            }
+                        }));
+            }
         });
         return id;
     }
@@ -497,6 +586,13 @@
             final int userId = options.getUserId();
             final int sensorId = options.getSensorId();
             final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId);
+            final LockoutTracker lockoutTracker;
+            if (Flags.deHidl()) {
+                lockoutTracker = mFingerprintSensors.get(sensorId)
+                        .getLockoutTracker(true /* forAuth */);
+            } else {
+                lockoutTracker = null;
+            }
             final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient(
                     mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, requestId,
                     callback, operationId, restricted, options, cookie,
@@ -510,7 +606,7 @@
                     mFingerprintSensors.get(sensorId).getSensorProperties(), mHandler,
                     Utils.getCurrentStrength(sensorId),
                     SystemClock.elapsedRealtimeClock(),
-                    null /* lockoutTracker */);
+                    lockoutTracker);
             scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
 
                 @Override
@@ -636,6 +732,9 @@
 
     @Override
     public boolean isHardwareDetected(int sensorId) {
+        if (Flags.deHidl()) {
+            return mFingerprintSensors.get(sensorId).isHardwareDetected(mHalInstanceName);
+        }
         return hasHalInstance();
     }
 
@@ -674,8 +773,12 @@
 
     @Override
     public int getLockoutModeForUser(int sensorId, int userId) {
-        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
-                Utils.getCurrentStrength(sensorId));
+        if (Flags.deHidl()) {
+            return mFingerprintSensors.get(sensorId).getLockoutModeForUser(userId);
+        } else {
+            return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                    Utils.getCurrentStrength(sensorId));
+        }
     }
 
     @Override
@@ -829,6 +932,10 @@
         mTestHalEnabled = enabled;
     }
 
+    public boolean getTestHalEnabled() {
+        return mTestHalEnabled;
+    }
+
     // TODO(b/174868353): workaround for gaps in HAL interface (remove and get directly from HAL)
     // reads values via an overlay instead of querying the HAL
     @NonNull
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
index ec225a6..387ae12 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
@@ -60,7 +60,8 @@
             @Authenticators.Types int biometricStrength) {
         super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
                 0 /* cookie */, sensorId, biometricLogger, biometricContext);
-        mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken);
+        mHardwareAuthToken = hardwareAuthToken == null ? null :
+                HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken);
         mLockoutCache = lockoutTracker;
         mLockoutResetDispatcher = lockoutResetDispatcher;
         mBiometricStrength = biometricStrength;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
index 893cb8f..dd887bb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
@@ -21,21 +21,28 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.hardware.biometrics.BiometricsProtoEnums;
+import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
+import android.hardware.biometrics.SensorLocationInternal;
+import android.hardware.biometrics.common.ComponentInfo;
+import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.ISession;
+import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.SensorServiceStateProto;
 import com.android.server.biometrics.SensorStateProto;
 import com.android.server.biometrics.UserStateProto;
@@ -48,15 +55,20 @@
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
 import com.android.server.biometrics.sensors.StartUserClient;
 import com.android.server.biometrics.sensors.StopUserClient;
 import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
 import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Maintains the state of a single sensor within an instance of the
@@ -73,15 +85,17 @@
     @NonNull private final IBinder mToken;
     @NonNull private final Handler mHandler;
     @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties;
-    @NonNull private final UserAwareBiometricScheduler mScheduler;
-    @NonNull private final LockoutCache mLockoutCache;
+    @NonNull private BiometricScheduler mScheduler;
+    @NonNull private LockoutTracker mLockoutTracker;
     @NonNull private final Map<Integer, Long> mAuthenticatorIds;
+    @NonNull private final BiometricContext mBiometricContext;
 
     @Nullable AidlSession mCurrentSession;
-    @NonNull private final Supplier<AidlSession> mLazySession;
+    @NonNull private Supplier<AidlSession> mLazySession;
 
-    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
-            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
+    public Sensor(@NonNull String tag, @NonNull FingerprintProvider provider,
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull FingerprintSensorPropertiesInternal sensorProperties,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext, AidlSession session) {
@@ -91,8 +105,38 @@
         mToken = new Binder();
         mHandler = handler;
         mSensorProperties = sensorProperties;
-        mLockoutCache = new LockoutCache();
-        mScheduler = new UserAwareBiometricScheduler(tag,
+        mBiometricContext = biometricContext;
+        mAuthenticatorIds = new HashMap<>();
+        mCurrentSession = session;
+    }
+
+    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext) {
+        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+                gestureAvailabilityDispatcher, biometricContext, null);
+    }
+
+    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+            @NonNull Handler handler, @NonNull SensorProps sensorProp,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            @NonNull List<SensorLocationInternal> workaroundLocation,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        this(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(sensorProp,
+                        workaroundLocation, resetLockoutRequiresHardwareAuthToken),
+                lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, null);
+    }
+
+    /**
+     * Initialize biometric scheduler, lockout tracker and session for the sensor.
+     */
+    public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            LockoutResetDispatcher lockoutResetDispatcher) {
+        mScheduler = new UserAwareBiometricScheduler(mTag,
                 BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties),
                 gestureAvailabilityDispatcher,
                 () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL,
@@ -102,7 +146,7 @@
                     public StopUserClient<?> getStopUserClient(int userId) {
                         return new FingerprintStopUserClient(mContext, mLazySession, mToken,
                                 userId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 () -> mCurrentSession = null);
                     }
 
@@ -111,13 +155,38 @@
                     public StartUserClient<?, ?> getStartUserClient(int newUserId) {
                         final int sensorId = mSensorProperties.sensorId;
 
-                        final AidlResponseHandler resultController = new AidlResponseHandler(
-                                mContext, mScheduler, sensorId, newUserId,
-                                mLockoutCache, lockoutResetDispatcher,
-                                biometricContext.getAuthSessionCoordinator(), () -> {
-                            Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
-                            mCurrentSession = null;
-                        });
+                        final AidlResponseHandler resultController;
+
+                        if (Flags.deHidl()) {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {},
+                                    new AidlResponseHandler.AidlResponseHandlerCallback() {
+                                        @Override
+                                        public void onEnrollSuccess() {
+                                            mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId,
+                                                    newUserId);
+                                            mProvider.scheduleInvalidationRequest(sensorId,
+                                                    newUserId);
+                                        }
+
+                                        @Override
+                                        public void onHardwareUnavailable() {
+                                            Slog.e(mTag,
+                                                    "Fingerprint sensor hardware unavailable.");
+                                            mCurrentSession = null;
+                                        }
+                                    });
+                        } else {
+                            resultController = new AidlResponseHandler(
+                                    mContext, mScheduler, sensorId, newUserId,
+                                    mLockoutTracker, lockoutResetDispatcher,
+                                    mBiometricContext.getAuthSessionCoordinator(), () -> {
+                                Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE");
+                                mCurrentSession = null;
+                            });
+                        }
 
                         final StartUserClient.UserStartedCallback<ISession> userStartedCallback =
                                 (userIdStarted, newSession, halInterfaceVersion) -> {
@@ -133,40 +202,58 @@
                                                         + "sensor: "
                                                         + sensorId
                                                         + ", user: " + userIdStarted);
-                                        provider.scheduleInvalidationRequest(sensorId,
+                                        mProvider.scheduleInvalidationRequest(sensorId,
                                                 userIdStarted);
                                     }
                                 };
 
-                        return new FingerprintStartUserClient(mContext, provider::getHalInstance,
+                        return new FingerprintStartUserClient(mContext, mProvider::getHalInstance,
                                 mToken, newUserId, mSensorProperties.sensorId,
-                                BiometricLogger.ofUnknown(mContext), biometricContext,
+                                BiometricLogger.ofUnknown(mContext), mBiometricContext,
                                 resultController, userStartedCallback);
                     }
                 });
-        mAuthenticatorIds = new HashMap<>();
+        mLockoutTracker = new LockoutCache();
         mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
-        mCurrentSession = session;
     }
 
-    Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
-            @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
-            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-            @NonNull BiometricContext biometricContext) {
-        this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
-                gestureAvailabilityDispatcher, biometricContext, null);
+    protected static FingerprintSensorPropertiesInternal getFingerprintSensorPropertiesInternal(
+            SensorProps prop, List<SensorLocationInternal> workaroundLocations,
+            boolean resetLockoutRequiresHardwareAuthToken) {
+        final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
+        if (prop.commonProps.componentInfo != null) {
+            for (ComponentInfo info : prop.commonProps.componentInfo) {
+                componentInfo.add(new ComponentInfoInternal(info.componentId,
+                        info.hardwareVersion, info.firmwareVersion, info.serialNumber,
+                        info.softwareVersion));
+            }
+        }
+        return new FingerprintSensorPropertiesInternal(prop.commonProps.sensorId,
+                prop.commonProps.sensorStrength,
+                prop.commonProps.maxEnrollmentsPerUser,
+                componentInfo,
+                prop.sensorType,
+                prop.halControlsIllumination,
+                resetLockoutRequiresHardwareAuthToken,
+                !workaroundLocations.isEmpty() ? workaroundLocations :
+                        Arrays.stream(prop.sensorLocations).map(location ->
+                                        new SensorLocationInternal(
+                                                location.display,
+                                                location.sensorLocationX,
+                                                location.sensorLocationY,
+                                                location.sensorRadius))
+                                .collect(Collectors.toList()));
     }
 
-    @NonNull Supplier<AidlSession> getLazySession() {
+    @NonNull public Supplier<AidlSession> getLazySession() {
         return mLazySession;
     }
 
-    @NonNull FingerprintSensorPropertiesInternal getSensorProperties() {
+    @NonNull public FingerprintSensorPropertiesInternal getSensorProperties() {
         return mSensorProperties;
     }
 
-    @Nullable AidlSession getSessionForUser(int userId) {
+    @Nullable protected AidlSession getSessionForUser(int userId) {
         if (mCurrentSession != null && mCurrentSession.getUserId() == userId) {
             return mCurrentSession;
         } else {
@@ -180,15 +267,18 @@
                 biometricStateCallback, mProvider, this);
     }
 
-    @NonNull BiometricScheduler getScheduler() {
+    @NonNull public BiometricScheduler getScheduler() {
         return mScheduler;
     }
 
-    @NonNull LockoutCache getLockoutCache() {
-        return mLockoutCache;
+    @NonNull protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        if (forAuth) {
+            return null;
+        }
+        return mLockoutTracker;
     }
 
-    @NonNull Map<Integer, Long> getAuthenticatorIds() {
+    @NonNull public Map<Integer, Long> getAuthenticatorIds() {
         return mAuthenticatorIds;
     }
 
@@ -262,4 +352,49 @@
         mScheduler.reset();
         mCurrentSession = null;
     }
+
+    @NonNull protected Handler getHandler() {
+        return mHandler;
+    }
+
+    @NonNull protected Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Returns true if the sensor hardware is detected.
+     */
+    protected boolean isHardwareDetected(String halInstance) {
+        if (mTestHalEnabled) {
+            return true;
+        }
+        return (ServiceManager.checkService(IFingerprint.DESCRIPTOR + "/" + halInstance)
+                != null);
+    }
+
+    @NonNull protected BiometricContext getBiometricContext() {
+        return mBiometricContext;
+    }
+
+    /**
+     * Returns lockout mode of this sensor.
+     */
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                Utils.getCurrentStrength(mSensorProperties.sensorId));
+    }
+
+    public void setScheduler(BiometricScheduler scheduler) {
+        mScheduler = scheduler;
+    }
+
+    public void setLazySession(
+            Supplier<AidlSession> lazySession) {
+        mLazySession = lazySession;
+    }
+
+    public FingerprintProvider getProvider() {
+        return mProvider;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
index a4e6025..5c5b992 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
@@ -29,7 +29,8 @@
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
-import com.android.server.biometrics.sensors.HalClientMonitor;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
 
 import java.io.File;
 import java.util.Map;
@@ -38,7 +39,8 @@
 /**
  * Sets the HAL's current active user, and updates the framework's authenticatorId cache.
  */
-public class FingerprintUpdateActiveUserClient extends HalClientMonitor<IBiometricsFingerprint> {
+public class FingerprintUpdateActiveUserClient extends
+        StartUserClient<IBiometricsFingerprint, AidlSession> {
 
     private static final String TAG = "FingerprintUpdateActiveUserClient";
     private static final String FP_DATA_DIR = "fpdata";
@@ -53,11 +55,24 @@
             @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId,
             @NonNull String owner, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            Supplier<Integer> currentUserId,
+            @NonNull Supplier<Integer> currentUserId,
             boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds,
             boolean forceUpdateAuthenticatorId) {
-        super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner,
-                0 /* cookie */, sensorId, logger, biometricContext);
+        this(context, lazyDaemon, userId, owner, sensorId, logger, biometricContext, currentUserId,
+                hasEnrolledBiometrics, authenticatorIds, forceUpdateAuthenticatorId,
+                (newUserId, newUser, halInterfaceVersion) -> {});
+    }
+
+    FingerprintUpdateActiveUserClient(@NonNull Context context,
+            @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId,
+            @NonNull String owner, int sensorId,
+            @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
+            @NonNull Supplier<Integer> currentUserId,
+            boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds,
+            boolean forceUpdateAuthenticatorId,
+            @NonNull UserStartedCallback<AidlSession> userStartedCallback) {
+        super(context, lazyDaemon, null /* token */, userId, sensorId, logger, biometricContext,
+                userStartedCallback);
         mCurrentUserId = currentUserId;
         mForceUpdateAuthenticatorId = forceUpdateAuthenticatorId;
         mHasEnrolledBiometrics = hasEnrolledBiometrics;
@@ -70,6 +85,7 @@
 
         if (mCurrentUserId.get() == getTargetUserId() && !mForceUpdateAuthenticatorId) {
             Slog.d(TAG, "Already user: " + mCurrentUserId + ", returning");
+            mUserStartedCallback.onUserStarted(getTargetUserId(), null, 0);
             callback.onClientFinished(this, true /* success */);
             return;
         }
@@ -119,6 +135,7 @@
             getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath());
             mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics
                     ? getFreshDaemon().getAuthenticatorId() : 0L);
+            mUserStartedCallback.onUserStarted(targetId, null, 0);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to setActiveGroup: " + e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java
index c3e5cbe..e9a48e7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlCallbackConverter.java
@@ -20,6 +20,7 @@
 import android.hardware.biometrics.fingerprint.V2_2.IBiometricsFingerprintClientCallback;
 
 import com.android.server.biometrics.HardwareAuthTokenUtils;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
 
 import java.util.ArrayList;
@@ -73,12 +74,12 @@
 
     @Override
     public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) {
-        mAidlResponseHandler.onEnrollmentsRemoved(new int[]{fingerId});
+        mAidlResponseHandler.onEnrollmentRemoved(fingerId, remaining);
     }
 
     @Override
     public void onEnumerate(long deviceId, int fingerId, int groupId, int remaining) {
-        mAidlResponseHandler.onEnrollmentsEnumerated(new int[]{fingerId});
+        mAidlResponseHandler.onEnrollmentEnumerated(fingerId, remaining);
     }
 
     void onChallengeGenerated(long challenge) {
@@ -92,4 +93,8 @@
     void onResetLockout() {
         mAidlResponseHandler.onLockoutCleared();
     }
+
+    <T extends BaseClientMonitor> void unsupportedClientScheduled(Class<T> className) {
+        mAidlResponseHandler.onUnsupportedClientScheduled(className);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
new file mode 100644
index 0000000..0bb61415
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
@@ -0,0 +1,297 @@
+/*
+ * 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.biometrics.sensors.fingerprint.hidl;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.os.Handler;
+import android.os.IHwBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.StopUserClient;
+import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils;
+import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
+import com.android.server.biometrics.sensors.fingerprint.aidl.Sensor;
+
+import java.util.ArrayList;
+
+/**
+ * Convert HIDL sensor configurations to an AIDL Sensor.
+ */
+public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRecipient {
+    private static final String TAG = "HidlToAidlSensorAdapter";
+
+    private final Runnable mInternalCleanupRunnable;
+    private final LockoutResetDispatcher mLockoutResetDispatcher;
+    private LockoutFrameworkImpl mLockoutTracker;
+    private final AuthSessionCoordinator mAuthSessionCoordinator;
+    private final AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    private int mCurrentUserId = UserHandle.USER_NULL;
+    private IBiometricsFingerprint mDaemon;
+    private AidlSession mSession;
+
+    private final StartUserClient.UserStartedCallback<AidlSession> mUserStartedCallback =
+            (newUserId, newUser, halInterfaceVersion) -> {
+                if (mCurrentUserId != newUserId) {
+                    handleUserChanged(newUserId);
+                }
+            };
+
+    public HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider,
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresHardwareAuthToken,
+            @NonNull Runnable internalCleanupRunnable) {
+        this(tag, provider, context, handler, prop, lockoutResetDispatcher,
+                gestureAvailabilityDispatcher, biometricContext,
+                resetLockoutRequiresHardwareAuthToken, internalCleanupRunnable,
+                new AuthSessionCoordinator(), null /* daemon */,
+                null /* onEnrollSuccessCallback */);
+    }
+
+    @VisibleForTesting
+    HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider,
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull SensorProps prop,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            boolean resetLockoutRequiresHardwareAuthToken,
+            @NonNull Runnable internalCleanupRunnable,
+            @NonNull AuthSessionCoordinator authSessionCoordinator,
+            @Nullable IBiometricsFingerprint daemon,
+            @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) {
+        super(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(prop,
+                        new ArrayList<>(), resetLockoutRequiresHardwareAuthToken),
+                lockoutResetDispatcher,
+                gestureAvailabilityDispatcher,
+                biometricContext, null /* session */);
+        mLockoutResetDispatcher = lockoutResetDispatcher;
+        mInternalCleanupRunnable = internalCleanupRunnable;
+        mAuthSessionCoordinator = authSessionCoordinator;
+        mDaemon = daemon;
+        mAidlResponseHandlerCallback = aidlResponseHandlerCallback == null
+                ? new AidlResponseHandler.AidlResponseHandlerCallback() {
+                    @Override
+                    public void onEnrollSuccess() {
+                        getScheduler()
+                                .scheduleClientMonitor(getFingerprintUpdateActiveUserClient(
+                                        mCurrentUserId, true /* forceUpdateAuthenticatorIds */));
+                    }
+
+                    @Override
+                    public void onHardwareUnavailable() {
+                        mDaemon = null;
+                        mSession = null;
+                        mCurrentUserId = UserHandle.USER_NULL;
+                    }
+                } : aidlResponseHandlerCallback;
+    }
+
+    @Override
+    public void serviceDied(long cookie) {
+        Slog.d(TAG, "HAL died.");
+        mSession = null;
+        mDaemon = null;
+    }
+
+    @Override
+    @LockoutTracker.LockoutMode
+    public int getLockoutModeForUser(int userId) {
+        return mLockoutTracker.getLockoutModeForUser(userId);
+    }
+
+    @Override
+    public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            LockoutResetDispatcher lockoutResetDispatcher) {
+        setLazySession(this::getSession);
+        setScheduler(new UserAwareBiometricScheduler(TAG,
+                BiometricScheduler.sensorTypeFromFingerprintProperties(getSensorProperties()),
+                gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback()));
+        mLockoutTracker = new LockoutFrameworkImpl(getContext(),
+                userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks(
+                        getSensorProperties().sensorId));
+    }
+
+    @Override
+    @Nullable
+    protected AidlSession getSessionForUser(int userId) {
+        if (mSession != null && mSession.getUserId() == userId) {
+            return mSession;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected boolean isHardwareDetected(String halInstance) {
+        return getIBiometricsFingerprint() != null;
+    }
+
+    @NonNull
+    @Override
+    protected LockoutTracker getLockoutTracker(boolean forAuth) {
+        return mLockoutTracker;
+    }
+
+    private synchronized AidlSession getSession() {
+        if (mSession != null && mDaemon != null) {
+            return mSession;
+        } else {
+            return mSession = new AidlSession(this::getIBiometricsFingerprint,
+                    mCurrentUserId, getAidlResponseHandler());
+        }
+    }
+
+    private AidlResponseHandler getAidlResponseHandler() {
+        return new AidlResponseHandler(getContext(),
+                getScheduler(),
+                getSensorProperties().sensorId,
+                mCurrentUserId,
+                mLockoutTracker,
+                mLockoutResetDispatcher,
+                mAuthSessionCoordinator,
+                () -> {}, mAidlResponseHandlerCallback);
+    }
+
+    @VisibleForTesting IBiometricsFingerprint getIBiometricsFingerprint() {
+        if (getProvider().getTestHalEnabled()) {
+            final TestHal testHal = new TestHal(getContext(), getSensorProperties().sensorId);
+            testHal.setNotify(new HidlToAidlCallbackConverter(getAidlResponseHandler()));
+            return testHal;
+        }
+
+        if (mDaemon != null) {
+            return mDaemon;
+        }
+
+        try {
+            mDaemon = IBiometricsFingerprint.getService();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get fingerprint HAL", e);
+        } catch (java.util.NoSuchElementException e) {
+            // Service doesn't exist or cannot be opened.
+            Slog.w(TAG, "NoSuchElementException", e);
+        }
+
+        if (mDaemon == null) {
+            Slog.w(TAG, "Fingerprint HAL not available");
+            return null;
+        }
+
+        mDaemon.asBinder().linkToDeath(this, 0 /* flags */);
+
+        Slog.d(TAG, "Fingerprint HAL ready");
+
+        scheduleLoadAuthenticatorIds();
+        mInternalCleanupRunnable.run();
+        return mDaemon;
+    }
+
+    private UserAwareBiometricScheduler.UserSwitchCallback getUserSwitchCallback() {
+        return new UserAwareBiometricScheduler.UserSwitchCallback() {
+            @NonNull
+            @Override
+            public StopUserClient<?> getStopUserClient(int userId) {
+                return new StopUserClient<IBiometricsFingerprint>(getContext(),
+                        HidlToAidlSensorAdapter.this::getIBiometricsFingerprint,
+                        null /* token */, userId, getSensorProperties().sensorId,
+                        BiometricLogger.ofUnknown(getContext()), getBiometricContext(),
+                        () -> {
+                            mCurrentUserId = UserHandle.USER_NULL;
+                            mSession = null;
+                        }) {
+                    @Override
+                    public void start(@NonNull ClientMonitorCallback callback) {
+                        super.start(callback);
+                        startHalOperation();
+                    }
+
+                    @Override
+                    protected void startHalOperation() {
+                        onUserStopped();
+                    }
+
+                    @Override
+                    public void unableToStart() {
+                        getCallback().onClientFinished(this, false /* success */);
+                    }
+                };
+            }
+
+            @NonNull
+            @Override
+            public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+                return getFingerprintUpdateActiveUserClient(newUserId,
+                        false /* forceUpdateAuthenticatorId */);
+            }
+        };
+    }
+
+    private FingerprintUpdateActiveUserClient getFingerprintUpdateActiveUserClient(int newUserId,
+            boolean forceUpdateAuthenticatorIds) {
+        return new FingerprintUpdateActiveUserClient(getContext(),
+                this::getIBiometricsFingerprint, newUserId, TAG,
+                getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()),
+                getBiometricContext(), () -> mCurrentUserId,
+                !FingerprintUtils.getInstance(getSensorProperties().sensorId)
+                        .getBiometricsForUser(getContext(),
+                newUserId).isEmpty(), getAuthenticatorIds(), forceUpdateAuthenticatorIds,
+                mUserStartedCallback);
+    }
+
+    private void scheduleLoadAuthenticatorIds() {
+        getHandler().post(() -> {
+            for (UserInfo user : UserManager.get(getContext()).getAliveUsers()) {
+                final int targetUserId = user.id;
+                if (!getAuthenticatorIds().containsKey(targetUserId)) {
+                    getScheduler().scheduleClientMonitor(getFingerprintUpdateActiveUserClient(
+                            targetUserId, true /* forceUpdateAuthenticatorIds */));
+                }
+            }
+        });
+    }
+
+    @VisibleForTesting void handleUserChanged(int newUserId) {
+        Slog.d(TAG, "User changed. Current user is " + newUserId);
+        mSession = null;
+        mCurrentUserId = newUserId;
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
similarity index 73%
rename from services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java
rename to services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
index b48d232..2fc00e1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
@@ -25,20 +25,25 @@
 import android.hardware.keymaster.HardwareAuthToken;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.Log;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.HardwareAuthTokenUtils;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
 import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintGetAuthenticatorIdClient;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintInvalidationClient;
 
 import java.util.function.Supplier;
 
 /**
- * Adapter to convert AIDL-specific interface {@link ISession} methods to HIDL implementation.
+ * Adapter to convert HIDL methods into AIDL interface {@link ISession}.
  */
-public class AidlToHidlAdapter implements ISession {
-    private final String TAG = "AidlToHidlAdapter";
+public class HidlToAidlSessionAdapter implements ISession {
+
+    private final String TAG = "HidlToAidlSessionAdapter";
+
     @VisibleForTesting
     static final int ENROLL_TIMEOUT_SEC = 60;
     @NonNull
@@ -46,22 +51,13 @@
     private final int mUserId;
     private HidlToAidlCallbackConverter mHidlToAidlCallbackConverter;
 
-    public AidlToHidlAdapter(Supplier<IBiometricsFingerprint> session, int userId,
+    public HidlToAidlSessionAdapter(Supplier<IBiometricsFingerprint> session, int userId,
             AidlResponseHandler aidlResponseHandler) {
         mSession = session;
         mUserId = userId;
         setCallback(aidlResponseHandler);
     }
 
-    private void setCallback(AidlResponseHandler aidlResponseHandler) {
-        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
-        try {
-            mSession.get().setNotify(mHidlToAidlCallbackConverter);
-        } catch (RemoteException e) {
-            Slog.d(TAG, "Failed to set callback");
-        }
-    }
-
     @Override
     public IBinder asBinder() {
         return null;
@@ -125,12 +121,16 @@
 
     @Override
     public void getAuthenticatorId() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "getAuthenticatorId unsupported in HIDL");
+        mHidlToAidlCallbackConverter.unsupportedClientScheduled(
+                FingerprintGetAuthenticatorIdClient.class);
     }
 
     @Override
     public void invalidateAuthenticatorId() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "invalidateAuthenticatorId unsupported in HIDL");
+        mHidlToAidlCallbackConverter.unsupportedClientScheduled(
+                FingerprintInvalidationClient.class);
     }
 
     @Override
@@ -140,72 +140,92 @@
 
     @Override
     public void close() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "close unsupported in HIDL");
     }
 
     @Override
     public void onUiReady() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onUiReady unsupported in HIDL");
     }
 
     @Override
     public ICancellationSignal authenticateWithContext(long operationId, OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Log.e(TAG, "authenticateWithContext unsupported in HIDL");
+        return authenticate(operationId);
     }
 
     @Override
     public ICancellationSignal enrollWithContext(HardwareAuthToken hat, OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Log.e(TAG, "enrollWithContext unsupported in HIDL");
+        return enroll(hat);
     }
 
     @Override
     public ICancellationSignal detectInteractionWithContext(OperationContext context)
             throws RemoteException {
-        //Unsupported in HIDL
-        return null;
+        Log.e(TAG, "enrollWithContext unsupported in HIDL");
+        return detectInteraction();
     }
 
     @Override
     public void onPointerDownWithContext(PointerContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onPointerDownWithContext unsupported in HIDL");
+        onPointerDown(context.pointerId, (int) context.x, (int) context.y, context.minor,
+                context.major);
     }
 
     @Override
     public void onPointerUpWithContext(PointerContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onPointerUpWithContext unsupported in HIDL");
+        onPointerUp(context.pointerId);
     }
 
     @Override
     public void onContextChanged(OperationContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onContextChanged unsupported in HIDL");
     }
 
     @Override
     public void onPointerCancelWithContext(PointerContext context) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "onPointerCancelWithContext unsupported in HIDL");
     }
 
     @Override
     public void setIgnoreDisplayTouches(boolean shouldIgnore) throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "setIgnoreDisplayTouches unsupported in HIDL");
     }
 
     @Override
     public int getInterfaceVersion() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "getInterfaceVersion unsupported in HIDL");
         return 0;
     }
 
     @Override
     public String getInterfaceHash() throws RemoteException {
-        //Unsupported in HIDL
+        Log.e(TAG, "getInterfaceHash unsupported in HIDL");
         return null;
     }
 
+    private void setCallback(AidlResponseHandler aidlResponseHandler) {
+        mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler);
+        try {
+            if (mSession.get() != null) {
+                long halId = mSession.get().setNotify(mHidlToAidlCallbackConverter);
+                Slog.d(TAG, "Fingerprint HAL ready, HAL ID: " + halId);
+                if (halId == 0) {
+                    Slog.d(TAG, "Unable to set HIDL callback.");
+                }
+            } else {
+                Slog.e(TAG, "Unable to set HIDL callback. HIDL daemon is null.");
+            }
+        } catch (RemoteException e) {
+            Slog.d(TAG, "Failed to set callback");
+        }
+    }
+
     private class Cancellation extends ICancellationSignal.Stub {
 
         Cancellation() {}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
index 0730c67..2f77275 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT;
 
+import android.annotation.NonNull;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -31,15 +32,18 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.sensors.LockoutTracker;
 
+import java.util.function.Function;
+
 /**
  * Tracks and enforces biometric lockout for biometric sensors that do not support lockout in the
  * HAL.
  */
 public class LockoutFrameworkImpl implements LockoutTracker {
 
-    private static final String TAG = "LockoutTracker";
+    private static final String TAG = "LockoutFrameworkImpl";
     private static final String ACTION_LOCKOUT_RESET =
             "com.android.server.biometrics.sensors.fingerprint.ACTION_LOCKOUT_RESET";
     private static final int MAX_FAILED_ATTEMPTS_LOCKOUT_TIMED = 5;
@@ -65,22 +69,32 @@
         void onLockoutReset(int userId);
     }
 
-    private final Context mContext;
     private final LockoutResetCallback mLockoutResetCallback;
     private final SparseBooleanArray mTimedLockoutCleared;
     private final SparseIntArray mFailedAttempts;
     private final AlarmManager mAlarmManager;
     private final LockoutReceiver mLockoutReceiver;
     private final Handler mHandler;
+    private final Function<Integer, PendingIntent> mLockoutResetIntent;
 
-    public LockoutFrameworkImpl(Context context, LockoutResetCallback lockoutResetCallback) {
-        mContext = context;
+    public LockoutFrameworkImpl(@NonNull Context context,
+            @NonNull LockoutResetCallback lockoutResetCallback) {
+        this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
+                new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+    }
+
+    @VisibleForTesting
+    LockoutFrameworkImpl(@NonNull Context context,
+            @NonNull LockoutResetCallback lockoutResetCallback,
+            @NonNull Function<Integer, PendingIntent> lockoutResetIntent) {
         mLockoutResetCallback = lockoutResetCallback;
         mTimedLockoutCleared = new SparseBooleanArray();
         mFailedAttempts = new SparseIntArray();
         mAlarmManager = context.getSystemService(AlarmManager.class);
         mLockoutReceiver = new LockoutReceiver();
         mHandler = new Handler(Looper.getMainLooper());
+        mLockoutResetIntent = lockoutResetIntent;
 
         context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET),
                 RESET_FINGERPRINT_LOCKOUT, null /* handler */, Context.RECEIVER_EXPORTED);
@@ -129,34 +143,18 @@
         return LOCKOUT_NONE;
     }
 
-    /**
-     * Clears lockout for Fingerprint HIDL HAL
-     */
     @Override
-    public void setLockoutModeForUser(int userId, int mode) {
-        mFailedAttempts.put(userId, 0);
-        mTimedLockoutCleared.put(userId, true);
-        // If we're asked to reset failed attempts externally (i.e. from Keyguard),
-        // the alarm might still be pending; remove it.
-        cancelLockoutResetForUser(userId);
-        mLockoutResetCallback.onLockoutReset(userId);
-    }
+    public void setLockoutModeForUser(int userId, int mode) {}
 
     private void cancelLockoutResetForUser(int userId) {
-        mAlarmManager.cancel(getLockoutResetIntentForUser(userId));
+        mAlarmManager.cancel(mLockoutResetIntent.apply(userId));
     }
 
     private void scheduleLockoutResetForUser(int userId) {
         mHandler.post(() -> {
             mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                SystemClock.elapsedRealtime() + FAIL_LOCKOUT_TIMEOUT_MS,
-                getLockoutResetIntentForUser(userId));
+                    SystemClock.elapsedRealtime() + FAIL_LOCKOUT_TIMEOUT_MS,
+                    mLockoutResetIntent.apply(userId));
         });
     }
-
-    private PendingIntent getLockoutResetIntentForUser(int userId) {
-        return PendingIntent.getBroadcast(mContext, userId,
-                new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-    }
 }
diff --git a/services/core/java/com/android/server/compat/CompatChange.java b/services/core/java/com/android/server/compat/CompatChange.java
index b5846b5..a40dd79 100644
--- a/services/core/java/com/android/server/compat/CompatChange.java
+++ b/services/core/java/com/android/server/compat/CompatChange.java
@@ -252,9 +252,6 @@
             // If the change is gated by a platform version newer than the one currently installed
             // on the device, disregard the app's target sdk version.
             int compareSdk = Math.min(app.targetSdkVersion, buildClassifier.platformTargetSdk());
-            if (compareSdk != app.targetSdkVersion) {
-                compareSdk = app.targetSdkVersion;
-            }
             return compareSdk >= getEnableSinceTargetSdk();
         }
         return true;
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index c517058..b7ece2ea 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -1299,7 +1299,7 @@
             }
 
             try {
-                mNms.denyProtect(mOwnerUID);
+                mNetd.networkSetProtectDeny(mOwnerUID);
             } catch (Exception e) {
                 Log.wtf(TAG, "Failed to disallow UID " + mOwnerUID + " to call protect() " + e);
             }
@@ -1309,7 +1309,7 @@
             mOwnerUID = getAppUid(mContext, newPackage, mUserId);
             mIsPackageTargetingAtLeastQ = doesPackageTargetAtLeastQ(newPackage);
             try {
-                mNms.allowProtect(mOwnerUID);
+                mNetd.networkSetProtectAllow(mOwnerUID);
             } catch (Exception e) {
                 Log.wtf(TAG, "Failed to allow UID " + mOwnerUID + " to call protect() " + e);
             }
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index b542893..3024dd2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -16,6 +16,10 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessModeToString;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityTaskManager;
 import android.app.ActivityTaskManager.RootTaskInfo;
@@ -40,6 +44,7 @@
 import android.util.EventLog;
 import android.util.MathUtils;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.TimeUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -49,6 +54,8 @@
 import com.android.server.display.brightness.BrightnessEvent;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Manages the associated display brightness when in auto-brightness mode. This is also
@@ -64,6 +71,18 @@
     public static final int AUTO_BRIGHTNESS_DISABLED = 2;
     public static final int AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE = 3;
 
+    @IntDef(prefix = { "AUTO_BRIGHTNESS_MODE_" }, value = {
+            AUTO_BRIGHTNESS_MODE_DEFAULT,
+            AUTO_BRIGHTNESS_MODE_IDLE,
+            AUTO_BRIGHTNESS_MODE_DOZE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AutomaticBrightnessMode{}
+
+    public static final int AUTO_BRIGHTNESS_MODE_DEFAULT = 0;
+    public static final int AUTO_BRIGHTNESS_MODE_IDLE = 1;
+    public static final int AUTO_BRIGHTNESS_MODE_DOZE = 2;
+
     // How long the current sensor reading is assumed to be valid beyond the current time.
     // This provides a bit of prediction, as well as ensures that the weight for the last sample is
     // non-zero, which in turn ensures that the total weight is non-zero.
@@ -91,10 +110,11 @@
     private final Sensor mLightSensor;
 
     // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
-    @Nullable
+    @NonNull
     private BrightnessMappingStrategy mCurrentBrightnessMapper;
-    private final BrightnessMappingStrategy mInteractiveModeBrightnessMapper;
-    private final BrightnessMappingStrategy mIdleModeBrightnessMapper;
+
+    // A map of Brightness Mapping Strategies indexed by AutomaticBrightnessMode
+    private final SparseArray<BrightnessMappingStrategy> mBrightnessMappingStrategyMap;
 
     // The minimum and maximum screen brightnesses.
     private final float mScreenBrightnessRangeMinimum;
@@ -251,7 +271,7 @@
 
     AutomaticBrightnessController(Callbacks callbacks, Looper looper,
             SensorManager sensorManager, Sensor lightSensor,
-            BrightnessMappingStrategy interactiveModeBrightnessMapper,
+            SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
             int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
             float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
             long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
@@ -261,26 +281,25 @@
             HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
-            BrightnessThrottler brightnessThrottler,
-            BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userBrightness) {
+            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+            int ambientLightHorizonLong, float userLux, float userNits) {
         this(new Injector(), callbacks, looper, sensorManager, lightSensor,
-                interactiveModeBrightnessMapper,
-                lightSensorWarmUpTime, brightnessMin, brightnessMax, dozeScaleFactor,
-                lightSensorRate, initialLightSensorRate, brighteningLightDebounceConfig,
-                darkeningLightDebounceConfig, brighteningLightDebounceConfigIdle,
-                darkeningLightDebounceConfigIdle, resetAmbientLuxAfterWarmUpConfig,
-                ambientBrightnessThresholds, screenBrightnessThresholds,
-                ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, context,
-                brightnessModeController, brightnessThrottler, idleModeBrightnessMapper,
-                ambientLightHorizonShort, ambientLightHorizonLong, userLux, userBrightness
+                brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
+                dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
+                resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                screenBrightnessThresholdsIdle, context, brightnessModeController,
+                brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                userNits
         );
     }
 
     @VisibleForTesting
     AutomaticBrightnessController(Injector injector, Callbacks callbacks, Looper looper,
             SensorManager sensorManager, Sensor lightSensor,
-            BrightnessMappingStrategy interactiveModeBrightnessMapper,
+            SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
             int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
             float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
             long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
@@ -290,15 +309,14 @@
             HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
-            BrightnessThrottler brightnessThrottler,
-            BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userBrightness) {
+            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+            int ambientLightHorizonLong, float userLux, float userNits) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
         mCallbacks = callbacks;
         mSensorManager = sensorManager;
-        mCurrentBrightnessMapper = interactiveModeBrightnessMapper;
+        mCurrentBrightnessMapper = brightnessMappingStrategyMap.get(AUTO_BRIGHTNESS_MODE_DEFAULT);
         mScreenBrightnessRangeMinimum = brightnessMin;
         mScreenBrightnessRangeMaximum = brightnessMax;
         mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
@@ -337,13 +355,12 @@
         mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
         mBrightnessRangeController = brightnessModeController;
         mBrightnessThrottler = brightnessThrottler;
-        mInteractiveModeBrightnessMapper = interactiveModeBrightnessMapper;
-        mIdleModeBrightnessMapper = idleModeBrightnessMapper;
-        // Initialize to active (normal) screen brightness mode
-        switchToInteractiveScreenBrightnessMode();
+        mBrightnessMappingStrategyMap = brightnessMappingStrategyMap;
 
         // Use the given short-term model
-        setScreenBrightnessByUser(userLux, userBrightness);
+        if (userNits != BrightnessMappingStrategy.INVALID_NITS) {
+            setScreenBrightnessByUser(userLux, getBrightnessFromNits(userNits));
+        }
     }
 
     /**
@@ -358,11 +375,8 @@
         if (mLoggingEnabled == loggingEnabled) {
             return false;
         }
-        if (mInteractiveModeBrightnessMapper != null) {
-            mInteractiveModeBrightnessMapper.setLoggingEnabled(loggingEnabled);
-        }
-        if (mIdleModeBrightnessMapper != null) {
-            mIdleModeBrightnessMapper.setLoggingEnabled(loggingEnabled);
+        for (int i = 0; i < mBrightnessMappingStrategyMap.size(); i++) {
+            mBrightnessMappingStrategyMap.valueAt(i).setLoggingEnabled(loggingEnabled);
         }
         mLoggingEnabled = loggingEnabled;
         return true;
@@ -389,8 +403,7 @@
                     | (!mAmbientLuxValid ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (mDisplayPolicy == DisplayPowerRequest.POLICY_DOZE
                         ? BrightnessEvent.FLAG_DOZE_SCALE : 0)
-                    | (mCurrentBrightnessMapper.isForIdleMode()
-                        ? BrightnessEvent.FLAG_IDLE_CURVE : 0));
+                    | (isInIdleMode() ? BrightnessEvent.FLAG_IDLE_CURVE : 0));
         }
 
         if (!mAmbientLuxValid) {
@@ -464,15 +477,13 @@
 
     // Used internally to establish whether we have deviated from the default config.
     public boolean isDefaultConfig() {
-        if (isInIdleMode()) {
-            return false;
-        }
-        return mInteractiveModeBrightnessMapper.isDefaultConfig();
+        return mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_DEFAULT
+                && mCurrentBrightnessMapper.isDefaultConfig();
     }
 
     // Called from APIs to get the configuration.
     public BrightnessConfiguration getDefaultConfig() {
-        return mInteractiveModeBrightnessMapper.getDefaultConfig();
+        return mBrightnessMappingStrategyMap.get(AUTO_BRIGHTNESS_MODE_DEFAULT).getDefaultConfig();
     }
 
     /**
@@ -527,8 +538,7 @@
     }
 
     private boolean setScreenBrightnessByUser(float lux, float brightness) {
-        if (lux == BrightnessMappingStrategy.NO_USER_LUX
-                || brightness == BrightnessMappingStrategy.NO_USER_BRIGHTNESS) {
+        if (lux == BrightnessMappingStrategy.INVALID_LUX || Float.isNaN(brightness)) {
             return false;
         }
         mCurrentBrightnessMapper.addUserDataPoint(lux, brightness);
@@ -543,7 +553,8 @@
 
     public boolean setBrightnessConfiguration(BrightnessConfiguration configuration,
             boolean shouldResetShortTermModel) {
-        if (mInteractiveModeBrightnessMapper.setBrightnessConfiguration(configuration)) {
+        if (mBrightnessMappingStrategyMap.get(AUTO_BRIGHTNESS_MODE_DEFAULT)
+                .setBrightnessConfiguration(configuration)) {
             if (!isInIdleMode() && shouldResetShortTermModel) {
                 resetShortTermModel();
             }
@@ -553,7 +564,7 @@
     }
 
     public boolean isInIdleMode() {
-        return mCurrentBrightnessMapper.isForIdleMode();
+        return mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE;
     }
 
     public void dump(PrintWriter pw) {
@@ -595,12 +606,6 @@
         pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
         pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
         pw.println("  mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy));
-        pw.println("  mShortTermModelTimeout(active)="
-                + mInteractiveModeBrightnessMapper.getShortTermModelTimeout());
-        if (mIdleModeBrightnessMapper != null) {
-            pw.println("  mShortTermModelTimeout(idle)="
-                    + mIdleModeBrightnessMapper.getShortTermModelTimeout());
-        }
         pw.println("  mShortTermModel=");
         mShortTermModel.dump(pw);
         pw.println("  mPausedShortTermModel=");
@@ -615,15 +620,15 @@
         pw.println("  mPendingForegroundAppPackageName=" + mPendingForegroundAppPackageName);
         pw.println("  mForegroundAppCategory=" + mForegroundAppCategory);
         pw.println("  mPendingForegroundAppCategory=" + mPendingForegroundAppCategory);
-        pw.println("  Idle mode active=" + mCurrentBrightnessMapper.isForIdleMode());
+        pw.println("  Current mode="
+                + autoBrightnessModeToString(mCurrentBrightnessMapper.getMode()));
 
         pw.println();
-        pw.println("  mInteractiveMapper=");
-        mInteractiveModeBrightnessMapper.dump(pw,
-                mBrightnessRangeController.getNormalBrightnessMax());
-        if (mIdleModeBrightnessMapper != null) {
-            pw.println("  mIdleMapper=");
-            mIdleModeBrightnessMapper.dump(pw, mBrightnessRangeController.getNormalBrightnessMax());
+        for (int i = 0; i < mBrightnessMappingStrategyMap.size(); i++) {
+            pw.println("  Mapper for mode "
+                    + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + "=");
+            mBrightnessMappingStrategyMap.valueAt(i).dump(pw,
+                    mBrightnessRangeController.getNormalBrightnessMax());
         }
 
         pw.println();
@@ -1117,84 +1122,71 @@
         updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
     }
 
-    void switchToIdleMode() {
-        if (mIdleModeBrightnessMapper == null) {
-            return;
-        }
-        if (mCurrentBrightnessMapper.isForIdleMode()) {
-            return;
-        }
-        Slog.i(TAG, "Switching to Idle Screen Brightness Mode");
+    private void switchModeAndShortTermModels(@AutomaticBrightnessMode int mode) {
         // Stash short term model
         ShortTermModel tempShortTermModel = new ShortTermModel();
         tempShortTermModel.set(mCurrentBrightnessMapper.getUserLux(),
                 mCurrentBrightnessMapper.getUserBrightness(), /* valid= */ true);
-
-        // Send delayed timeout
-        mHandler.sendEmptyMessageAtTime(MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL,
-                mClock.uptimeMillis()
-                        + mCurrentBrightnessMapper.getShortTermModelTimeout());
-
-        Slog.i(TAG, "mPreviousShortTermModel" + mPausedShortTermModel);
-        // new brightness mapper
-        mCurrentBrightnessMapper = mIdleModeBrightnessMapper;
-
-        // if previous stm has been invalidated, and lux has drastically changed, just use
-        // the new, reset stm.
-        // if previous stm is still valid then revalidate it
-        if (mPausedShortTermModel != null && !mPausedShortTermModel.maybeReset(mAmbientLux)) {
-            setScreenBrightnessByUser(mPausedShortTermModel.mAnchor,
-                    mPausedShortTermModel.mBrightness);
-        }
-        mPausedShortTermModel.copyFrom(tempShortTermModel);
-
-        update();
-    }
-
-    void switchToInteractiveScreenBrightnessMode() {
-        if (!mCurrentBrightnessMapper.isForIdleMode()) {
-            return;
-        }
-        Slog.i(TAG, "Switching to Interactive Screen Brightness Mode");
-        ShortTermModel tempShortTermModel = new ShortTermModel();
-        tempShortTermModel.set(mCurrentBrightnessMapper.getUserLux(),
-                mCurrentBrightnessMapper.getUserBrightness(), /* valid= */ true);
         mHandler.removeMessages(MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL);
         // Send delayed timeout
         mHandler.sendEmptyMessageAtTime(MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL,
                 mClock.uptimeMillis()
                         + mCurrentBrightnessMapper.getShortTermModelTimeout());
-        Slog.i(TAG, "mPreviousShortTermModel" + mPausedShortTermModel.toString());
 
-        // restore interactive mapper.
-        mCurrentBrightnessMapper = mInteractiveModeBrightnessMapper;
+        Slog.i(TAG, "mPreviousShortTermModel: " + mPausedShortTermModel);
+        // new brightness mapper
+        mCurrentBrightnessMapper = mBrightnessMappingStrategyMap.get(mode);
 
         // if previous stm has been invalidated, and lux has drastically changed, just use
         // the new, reset stm.
         // if previous stm is still valid then revalidate it
-        if (!mPausedShortTermModel.maybeReset(mAmbientLux)) {
-            setScreenBrightnessByUser(mPausedShortTermModel.mAnchor,
-                    mPausedShortTermModel.mBrightness);
+        if (mPausedShortTermModel != null) {
+            if (!mPausedShortTermModel.maybeReset(mAmbientLux)) {
+                setScreenBrightnessByUser(mPausedShortTermModel.mAnchor,
+                        mPausedShortTermModel.mBrightness);
+            }
+            mPausedShortTermModel.copyFrom(tempShortTermModel);
         }
-        mPausedShortTermModel.copyFrom(tempShortTermModel);
 
         update();
     }
 
+    void switchMode(@AutomaticBrightnessMode int mode) {
+        if (!mBrightnessMappingStrategyMap.contains(mode)) {
+            return;
+        }
+        if (mCurrentBrightnessMapper.getMode() == mode) {
+            return;
+        }
+        Slog.i(TAG, "Switching to mode " + mode);
+        if (mode == AUTO_BRIGHTNESS_MODE_IDLE
+                || mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) {
+            switchModeAndShortTermModels(mode);
+        } else {
+            resetShortTermModel();
+            mCurrentBrightnessMapper = mBrightnessMappingStrategyMap.get(mode);
+        }
+    }
+
+    float getUserLux() {
+        return mCurrentBrightnessMapper.getUserLux();
+    }
+
+    float getUserNits() {
+        return convertToNits(mCurrentBrightnessMapper.getUserBrightness());
+    }
+
     /**
      * Convert a brightness float scale value to a nit value. Adjustments, such as RBC, are not
      * applied. This is used when storing the brightness in nits for the default display and when
      * passing the brightness value to follower displays.
      *
      * @param brightness The float scale value
-     * @return The nit value or -1f if no conversion is possible.
+     * @return The nit value or {@link BrightnessMappingStrategy.INVALID_NITS} if no conversion is
+     * possible.
      */
     public float convertToNits(float brightness) {
-        if (mCurrentBrightnessMapper != null) {
-            return mCurrentBrightnessMapper.convertToNits(brightness);
-        } else {
-            return -1.0f;
-        }
+        return mCurrentBrightnessMapper.convertToNits(brightness);
     }
 
     /**
@@ -1203,14 +1195,11 @@
      * {@link com.android.server.display.BrightnessTracker}.
      *
      * @param brightness The float scale value
-     * @return The nit value or -1f if no conversion is possible.
+     * @return The nit value or {@link BrightnessMappingStrategy.INVALID_NITS} if no conversion is
+     * possible.
      */
     public float convertToAdjustedNits(float brightness) {
-        if (mCurrentBrightnessMapper != null) {
-            return mCurrentBrightnessMapper.convertToAdjustedNits(brightness);
-        } else {
-            return -1.0f;
-        }
+        return mCurrentBrightnessMapper.convertToAdjustedNits(brightness);
     }
 
     /**
@@ -1221,12 +1210,8 @@
      * @return The float scale value or {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if no
      * conversion is possible.
      */
-    public float convertToFloatScale(float nits) {
-        if (mCurrentBrightnessMapper != null) {
-            return mCurrentBrightnessMapper.convertToFloatScale(nits);
-        } else {
-            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        }
+    public float getBrightnessFromNits(float nits) {
+        return mCurrentBrightnessMapper.getBrightnessFromNits(nits);
     }
 
     public void recalculateSplines(boolean applyAdjustment, float[] adjustment) {
@@ -1250,13 +1235,13 @@
         // light.
         // The anchor determines what were the light levels when the user has set their preference,
         // and we use a relative threshold to determine when to revert to the OEM curve.
-        private float mAnchor = BrightnessMappingStrategy.NO_USER_LUX;
-        private float mBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
+        private float mAnchor = BrightnessMappingStrategy.INVALID_LUX;
+        private float mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         private boolean mIsValid = false;
 
         private void reset() {
-            mAnchor = BrightnessMappingStrategy.NO_USER_LUX;
-            mBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
+            mAnchor = BrightnessMappingStrategy.INVALID_LUX;
+            mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mIsValid = false;
         }
 
@@ -1279,10 +1264,8 @@
         private boolean maybeReset(float currentLux) {
             // If the short term model was invalidated and the change is drastic enough, reset it.
             // Otherwise, we revalidate it.
-            if (!mIsValid && mAnchor != -1) {
-                if (mCurrentBrightnessMapper != null
-                        && mCurrentBrightnessMapper.shouldResetShortTermModel(
-                        currentLux, mAnchor)) {
+            if (!mIsValid && mAnchor != BrightnessMappingStrategy.INVALID_LUX) {
+                if (mCurrentBrightnessMapper.shouldResetShortTermModel(currentLux, mAnchor)) {
                     resetShortTermModel();
                 } else {
                     mIsValid = true;
@@ -1304,7 +1287,7 @@
         }
 
         public String toString() {
-            return " mAnchor: " + mAnchor
+            return "mAnchor: " + mAnchor
                     + "\n mBrightness: " + mBrightness
                     + "\n mIsValid: " + mIsValid;
         }
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index d848f4b..8405e0a 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -18,13 +18,19 @@
 
 import static android.text.TextUtils.formatSimple;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+
 import android.annotation.Nullable;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
-import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.BrightnessCorrection;
 import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.LongArray;
 import android.util.MathUtils;
 import android.util.Pair;
@@ -32,7 +38,6 @@
 import android.util.Spline;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.display.BrightnessUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.display.utils.Plog;
@@ -57,8 +62,8 @@
 public abstract class BrightnessMappingStrategy {
     private static final String TAG = "BrightnessMappingStrategy";
 
-    public static final float NO_USER_LUX = -1;
-    public static final float NO_USER_BRIGHTNESS = -1;
+    public static final float INVALID_LUX = -1;
+    public static final float INVALID_NITS = -1;
 
     private static final float LUX_GRAD_SMOOTHING = 0.25f;
     private static final float MAX_GRAD = 1.0f;
@@ -74,82 +79,53 @@
     private static final Plog PLOG = Plog.createSystemPlog(TAG);
 
     /**
-     * Creates a BrightnessMappingStrategy for active (normal) mode.
-     * @param resources
-     * @param displayDeviceConfig
-     * @param displayWhiteBalanceController
-     * @return the BrightnessMappingStrategy
-     */
-    @Nullable
-    public static BrightnessMappingStrategy create(Resources resources,
-            DisplayDeviceConfig displayDeviceConfig,
-            DisplayWhiteBalanceController displayWhiteBalanceController) {
-        return create(resources, displayDeviceConfig, /* isForIdleMode= */ false,
-                displayWhiteBalanceController);
-    }
-
-    /**
-     * Creates a BrightnessMappingStrategy for idle screen brightness mode.
-     * @param resources
-     * @param displayDeviceConfig
-     * @param displayWhiteBalanceController
-     * @return the BrightnessMappingStrategy
-     */
-    @Nullable
-    public static BrightnessMappingStrategy createForIdleMode(Resources resources,
-            DisplayDeviceConfig displayDeviceConfig, DisplayWhiteBalanceController
-            displayWhiteBalanceController) {
-        return create(resources, displayDeviceConfig, /* isForIdleMode= */ true,
-                displayWhiteBalanceController);
-    }
-
-    /**
-     * Creates a BrightnessMapping strategy for either active or idle screen brightness mode.
-     * We do not create a simple mapping strategy for idle mode.
+     * Creates a BrightnessMapping strategy. We do not create a simple mapping strategy for idle
+     * mode.
      *
-     * @param resources
+     * @param context
      * @param displayDeviceConfig
-     * @param isForIdleMode determines whether the configurations loaded are for idle screen
-     *                      brightness mode or active screen brightness mode.
+     * @param mode The auto-brightness mode. Different modes use different brightness curves
      * @param displayWhiteBalanceController
      * @return the BrightnessMappingStrategy
      */
     @Nullable
-    private static BrightnessMappingStrategy create(Resources resources,
-            DisplayDeviceConfig displayDeviceConfig, boolean isForIdleMode,
-            DisplayWhiteBalanceController displayWhiteBalanceController) {
+    static BrightnessMappingStrategy create(Context context,
+            DisplayDeviceConfig displayDeviceConfig,
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode,
+            @Nullable DisplayWhiteBalanceController displayWhiteBalanceController) {
 
         // Display independent, mode dependent values
-        float[] brightnessLevelsNits;
+        float[] brightnessLevelsNits = null;
         float[] brightnessLevels = null;
-        float[] luxLevels;
-        if (isForIdleMode) {
-            brightnessLevelsNits = getFloatArray(resources.obtainTypedArray(
-                    com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle));
-            luxLevels = getLuxLevels(resources.getIntArray(
-                    com.android.internal.R.array.config_autoBrightnessLevelsIdle));
-        } else {
-            brightnessLevelsNits = displayDeviceConfig.getAutoBrightnessBrighteningLevelsNits();
-            luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux();
-
-            brightnessLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevels();
-            if (brightnessLevels == null || brightnessLevels.length == 0) {
-                // Load the old configuration in the range [0, 255]. The values need to be
-                // normalized to the range [0, 1].
-                int[] brightnessLevelsInt = resources.getIntArray(
-                        com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
-                brightnessLevels = new float[brightnessLevelsInt.length];
-                for (int i = 0; i < brightnessLevels.length; i++) {
-                    brightnessLevels[i] = normalizeAbsoluteBrightness(brightnessLevelsInt[i]);
-                }
+        float[] luxLevels = null;
+        int preset = Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+        switch (mode) {
+            case AUTO_BRIGHTNESS_MODE_DEFAULT -> {
+                brightnessLevelsNits = displayDeviceConfig.getAutoBrightnessBrighteningLevelsNits();
+                luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
+                brightnessLevels =
+                        displayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
+            }
+            case AUTO_BRIGHTNESS_MODE_IDLE -> {
+                brightnessLevelsNits = getFloatArray(context.getResources().obtainTypedArray(
+                        com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle));
+                luxLevels = getLuxLevels(context.getResources().getIntArray(
+                        com.android.internal.R.array.config_autoBrightnessLevelsIdle));
+            }
+            case AUTO_BRIGHTNESS_MODE_DOZE -> {
+                luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
+                brightnessLevels =
+                        displayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
             }
         }
 
         // Display independent, mode independent values
-        float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
+        float autoBrightnessAdjustmentMaxGamma = context.getResources().getFraction(
                 com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
                 1, 1);
-        long shortTermModelTimeout = resources.getInteger(
+        long shortTermModelTimeout = context.getResources().getInteger(
                 com.android.internal.R.integer.config_autoBrightnessShortTermModelTimeout);
 
         // Display dependent values - used for physical mapping strategy nits -> brightness
@@ -158,17 +134,16 @@
 
         if (isValidMapping(nitsRange, brightnessRange)
                 && isValidMapping(luxLevels, brightnessLevelsNits)) {
-
             BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(
                     luxLevels, brightnessLevelsNits);
             builder.setShortTermModelTimeoutMillis(shortTermModelTimeout);
             builder.setShortTermModelLowerLuxMultiplier(SHORT_TERM_MODEL_THRESHOLD_RATIO);
             builder.setShortTermModelUpperLuxMultiplier(SHORT_TERM_MODEL_THRESHOLD_RATIO);
             return new PhysicalMappingStrategy(builder.build(), nitsRange, brightnessRange,
-                    autoBrightnessAdjustmentMaxGamma, isForIdleMode, displayWhiteBalanceController);
+                    autoBrightnessAdjustmentMaxGamma, mode, displayWhiteBalanceController);
         } else if (isValidMapping(luxLevels, brightnessLevels)) {
             return new SimpleMappingStrategy(luxLevels, brightnessLevels,
-                    autoBrightnessAdjustmentMaxGamma, shortTermModelTimeout);
+                    autoBrightnessAdjustmentMaxGamma, shortTermModelTimeout, mode);
         } else {
             return null;
         }
@@ -334,7 +309,7 @@
     /**
      * Converts the provided brightness value to nits if possible.
      *
-     * Returns -1.0f if there's no available mapping for the brightness to nits.
+     * Returns {@link INVALID_NITS} if there's no available mapping for the brightness to nits.
      */
     public abstract float convertToNits(float brightness);
 
@@ -342,7 +317,7 @@
      * Converts the provided brightness value to nits if possible. Adjustments, such as RBC are
      * applied.
      *
-     * Returns -1.0f if there's no available mapping for the brightness to nits.
+     * Returns {@link INVALID_NITS} if there's no available mapping for the brightness to nits.
      */
     public abstract float convertToAdjustedNits(float brightness);
 
@@ -352,7 +327,7 @@
      * Returns {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if there's no available mapping for
      * the nits to float scale.
      */
-    public abstract float convertToFloatScale(float nits);
+    public abstract float getBrightnessFromNits(float nits);
 
     /**
      * Adds a user interaction data point to the brightness mapping.
@@ -407,17 +382,18 @@
      */
     public abstract void dump(PrintWriter pw, float hbmTransition);
 
-    /**
-     * We can designate a mapping strategy to be used for idle screen brightness mode.
-     * @return whether this mapping strategy is to be used for idle screen brightness mode.
-     */
-    public abstract boolean isForIdleMode();
-
     abstract float getUserLux();
 
     abstract float getUserBrightness();
 
     /**
+     * @return The auto-brightness mode of this mapping strategy. Different modes use different
+     * brightness curves.
+     */
+    @AutomaticBrightnessController.AutomaticBrightnessMode
+    abstract int getMode();
+
+    /**
      * Check if the short term model should be reset given the anchor lux the last
      * brightness change was made at and the current ambient lux.
      */
@@ -450,11 +426,6 @@
         }
     }
 
-    // Normalize entire brightness range to 0 - 1.
-    protected static float normalizeAbsoluteBrightness(int brightness) {
-        return BrightnessSynchronizer.brightnessIntToFloat(brightness);
-    }
-
     private Pair<float[], float[]> insertControlPoint(
             float[] luxLevels, float[] brightnessLevels, float lux, float brightness) {
         final int idx = findInsertionPoint(luxLevels, lux);
@@ -596,7 +567,7 @@
         if (mLoggingEnabled) {
             PLOG.logCurve("gamma adjusted curve", newLux, newBrightness);
         }
-        if (userLux != -1) {
+        if (userLux != INVALID_LUX) {
             Pair<float[], float[]> curve = insertControlPoint(newLux, newBrightness, userLux,
                     userBrightness);
             newLux = curve.first;
@@ -624,6 +595,9 @@
         // Brightness control points normalized to [0, 1]
         private final float[] mBrightness;
 
+        @AutomaticBrightnessController.AutomaticBrightnessMode
+        private final int mMode;
+
         private Spline mSpline;
         private float mMaxGamma;
         private float mAutoBrightnessAdjustment;
@@ -632,7 +606,7 @@
         private long mShortTermModelTimeout;
 
         private SimpleMappingStrategy(float[] lux, float[] brightness, float maxGamma,
-                long timeout) {
+                long timeout, @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
             Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
                     "Lux and brightness arrays must not be empty!");
             Preconditions.checkArgument(lux.length == brightness.length,
@@ -651,13 +625,14 @@
 
             mMaxGamma = maxGamma;
             mAutoBrightnessAdjustment = 0;
-            mUserLux = NO_USER_LUX;
-            mUserBrightness = NO_USER_BRIGHTNESS;
+            mUserLux = INVALID_LUX;
+            mUserBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             if (mLoggingEnabled) {
                 PLOG.start("simple mapping strategy");
             }
             computeSpline();
             mShortTermModelTimeout = timeout;
+            mMode = mode;
         }
 
         @Override
@@ -704,16 +679,16 @@
 
         @Override
         public float convertToNits(float brightness) {
-            return -1.0f;
+            return INVALID_NITS;
         }
 
         @Override
         public float convertToAdjustedNits(float brightness) {
-            return -1.0f;
+            return INVALID_NITS;
         }
 
         @Override
-        public float convertToFloatScale(float nits) {
+        public float getBrightnessFromNits(float nits) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
 
@@ -741,22 +716,22 @@
 
         @Override
         public void clearUserDataPoints() {
-            if (mUserLux != -1) {
+            if (mUserLux != INVALID_LUX) {
                 if (mLoggingEnabled) {
                     Slog.d(TAG, "clearUserDataPoints: " + mAutoBrightnessAdjustment + " => 0");
                     PLOG.start("clear user data points")
                             .logPoint("user data point", mUserLux, mUserBrightness);
                 }
                 mAutoBrightnessAdjustment = 0;
-                mUserLux = -1;
-                mUserBrightness = -1;
+                mUserLux = INVALID_LUX;
+                mUserBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
                 computeSpline();
             }
         }
 
         @Override
         public boolean hasUserDataPoints() {
-            return mUserLux != -1;
+            return mUserLux != INVALID_LUX;
         }
 
         @Override
@@ -782,11 +757,12 @@
             pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
             pw.println("  mUserLux=" + mUserLux);
             pw.println("  mUserBrightness=" + mUserBrightness);
+            pw.println("  mShortTermModelTimeout=" + mShortTermModelTimeout);
         }
 
         @Override
-        public boolean isForIdleMode() {
-            return false;
+        int getMode() {
+            return mMode;
         }
 
         @Override
@@ -854,9 +830,13 @@
         private float mAutoBrightnessAdjustment;
         private float mUserLux;
         private float mUserBrightness;
-        private final boolean mIsForIdleMode;
+
+        @Nullable
         private final DisplayWhiteBalanceController mDisplayWhiteBalanceController;
 
+        @AutomaticBrightnessController.AutomaticBrightnessMode
+        private final int mMode;
+
         // Previous short-term models and the times that they were computed stored for debugging
         // purposes
         private List<Spline> mPreviousBrightnessSplines = new ArrayList<>();
@@ -865,8 +845,9 @@
         private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
 
         public PhysicalMappingStrategy(BrightnessConfiguration config, float[] nits,
-                float[] brightness, float maxGamma, boolean isForIdleMode,
-                DisplayWhiteBalanceController displayWhiteBalanceController) {
+                float[] brightness, float maxGamma,
+                @AutomaticBrightnessController.AutomaticBrightnessMode int mode,
+                @Nullable DisplayWhiteBalanceController displayWhiteBalanceController) {
 
             Preconditions.checkArgument(nits.length != 0 && brightness.length != 0,
                     "Nits and brightness arrays must not be empty!");
@@ -878,11 +859,11 @@
             Preconditions.checkArrayElementsInRange(brightness,
                     PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, "brightness");
 
-            mIsForIdleMode = isForIdleMode;
+            mMode = mode;
             mMaxGamma = maxGamma;
             mAutoBrightnessAdjustment = 0;
-            mUserLux = NO_USER_LUX;
-            mUserBrightness = NO_USER_BRIGHTNESS;
+            mUserLux = INVALID_LUX;
+            mUserBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mDisplayWhiteBalanceController = displayWhiteBalanceController;
 
             mNits = nits;
@@ -982,7 +963,7 @@
         }
 
         @Override
-        public float convertToFloatScale(float nits) {
+        public float getBrightnessFromNits(float nits) {
             return mNitsToBrightnessSpline.interpolate(nits);
         }
 
@@ -1024,15 +1005,15 @@
                             .logPoint("user data point", mUserLux, mUserBrightness);
                 }
                 mAutoBrightnessAdjustment = 0;
-                mUserLux = -1;
-                mUserBrightness = -1;
+                mUserLux = INVALID_LUX;
+                mUserBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
                 computeSpline();
             }
         }
 
         @Override
         public boolean hasUserDataPoints() {
-            return mUserLux != -1;
+            return mUserLux != INVALID_LUX;
         }
 
         @Override
@@ -1072,6 +1053,7 @@
             pw.println("  mUserBrightness=" + mUserBrightness);
             pw.println("  mDefaultConfig=" + mDefaultConfig);
             pw.println("  mBrightnessRangeAdjustmentApplied=" + mBrightnessRangeAdjustmentApplied);
+            pw.println("  shortTermModelTimeout=" + getShortTermModelTimeout());
 
             pw.println("  Previous short-term models (oldest to newest): ");
             for (int i = 0; i < mPreviousBrightnessSplines.size(); i++) {
@@ -1086,8 +1068,8 @@
         }
 
         @Override
-        public boolean isForIdleMode() {
-            return mIsForIdleMode;
+        int getMode() {
+            return mMode;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index d97127c..bd22e1d 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -49,6 +49,7 @@
 import com.android.server.display.config.BrightnessThrottlingMap;
 import com.android.server.display.config.BrightnessThrottlingPoint;
 import com.android.server.display.config.Density;
+import com.android.server.display.config.DisplayBrightnessMappingConfig;
 import com.android.server.display.config.DisplayBrightnessPoint;
 import com.android.server.display.config.DisplayConfiguration;
 import com.android.server.display.config.DisplayQuirks;
@@ -57,7 +58,6 @@
 import com.android.server.display.config.HighBrightnessMode;
 import com.android.server.display.config.IntegerArray;
 import com.android.server.display.config.LuxThrottling;
-import com.android.server.display.config.LuxToBrightnessMapping;
 import com.android.server.display.config.NitsMap;
 import com.android.server.display.config.NonNegativeFloatToFloatPoint;
 import com.android.server.display.config.Point;
@@ -313,6 +313,21 @@
  *              1000
  *          </darkeningLightDebounceIdleMillis>
  *          <luxToBrightnessMapping>
+ *            <mode>default</mode>
+ *            <map>
+ *              <point>
+ *                <first>0</first>
+ *                <second>0.2</second>
+ *              </point>
+ *              <point>
+ *                <first>80</first>
+ *                <second>0.3</second>
+ *              </point>
+ *            </map>
+ *          </luxToBrightnessMapping>
+ *          <luxToBrightnessMapping>
+ *            <mode>doze</mode>
+ *            <setting>dim</setting>
  *            <map>
  *              <point>
  *                <first>0</first>
@@ -634,36 +649,8 @@
     // for the corresponding values above
     private float[] mBrightness;
 
-    /**
-     * Array of desired screen brightness in nits corresponding to the lux values
-     * in the mBrightnessLevelsLux array. The display brightness is defined as the
-     * measured brightness of an all-white image. The brightness values must be non-negative and
-     * non-decreasing. This must be overridden in platform specific overlays
-     */
-    private float[] mBrightnessLevelsNits;
-
-    /**
-     * Array of desired screen brightness corresponding to the lux values
-     * in the mBrightnessLevelsLux array. The brightness values must be non-negative and
-     * non-decreasing. They must be between {@link PowerManager.BRIGHTNESS_MIN} and
-     * {@link PowerManager.BRIGHTNESS_MAX}. This must be overridden in platform specific overlays
-     */
-    private float[] mBrightnessLevels;
-
-    /**
-     * Array of light sensor lux values to define our levels for auto-brightness support.
-     *
-     * The first lux value is always 0.
-     *
-     * The control points must be strictly increasing. Each control point corresponds to an entry
-     * in the brightness values arrays. For example, if lux == luxLevels[1] (second element
-     * of the levels array) then the brightness will be determined by brightnessLevels[1] (second
-     * element of the brightness values array).
-     *
-     * Spline interpolation is used to determine the auto-brightness values for lux levels between
-     * these control points.
-     */
-    private float[] mBrightnessLevelsLux;
+    @Nullable
+    private DisplayBrightnessMappingConfig mDisplayBrightnessMapping;
 
     private float mBacklightMinimum = Float.NaN;
     private float mBacklightMaximum = Float.NaN;
@@ -1604,24 +1591,42 @@
     }
 
     /**
-     * @return Auto brightness brightening ambient lux levels
+     * @param mode The auto-brightness mode
+     * @param preset The brightness preset. Presets are used on devices that allow users to choose
+     *               from a set of predefined options in display auto-brightness settings.
+     * @return The default auto-brightness brightening ambient lux levels for the specified mode
+     * and preset
      */
-    public float[] getAutoBrightnessBrighteningLevelsLux() {
-        return mBrightnessLevelsLux;
+    public float[] getAutoBrightnessBrighteningLevelsLux(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) {
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getLuxArray(mode, preset);
     }
 
     /**
      * @return Auto brightness brightening nits levels
      */
     public float[] getAutoBrightnessBrighteningLevelsNits() {
-        return mBrightnessLevelsNits;
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getNitsArray();
     }
 
     /**
-     * @return Auto brightness brightening levels
+     * @param mode The auto-brightness mode
+     * @param preset The brightness preset. Presets are used on devices that allow users to choose
+     *               from a set of predefined options in display auto-brightness settings.
+     * @return The default auto-brightness brightening levels for the specified mode and preset
      */
-    public float[] getAutoBrightnessBrighteningLevels() {
-        return mBrightnessLevels;
+    public float[] getAutoBrightnessBrighteningLevels(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) {
+        if (mDisplayBrightnessMapping == null) {
+            return null;
+        }
+        return mDisplayBrightnessMapping.getBrightnessArray(mode, preset);
     }
 
     /**
@@ -1875,9 +1880,7 @@
                 + mAutoBrightnessBrighteningLightDebounceIdle
                 + ", mAutoBrightnessDarkeningLightDebounceIdle= "
                 + mAutoBrightnessDarkeningLightDebounceIdle
-                + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux)
-                + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits)
-                + ", mBrightnessLevels= " + Arrays.toString(mBrightnessLevels)
+                + ", mDisplayBrightnessMapping= " + mDisplayBrightnessMapping
                 + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable
                 + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable
                 + "\n"
@@ -2568,7 +2571,8 @@
         // Idle must be called after interactive, since we fall back to it if needed.
         loadAutoBrightnessBrighteningLightDebounceIdle(autoBrightness);
         loadAutoBrightnessDarkeningLightDebounceIdle(autoBrightness);
-        loadAutoBrightnessDisplayBrightnessMapping(autoBrightness);
+        mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
+                autoBrightness, mBacklightToBrightnessSpline);
         loadEnableAutoBrightness(autoBrightness);
     }
 
@@ -2633,38 +2637,6 @@
         }
     }
 
-    /**
-     * Loads the auto-brightness display brightness mappings. Internally, this takes care of
-     * loading the value from the display config, and if not present, falls back to config.xml.
-     */
-    private void loadAutoBrightnessDisplayBrightnessMapping(AutoBrightness autoBrightnessConfig) {
-        if (mFlags.areAutoBrightnessModesEnabled() && autoBrightnessConfig != null
-                && autoBrightnessConfig.getLuxToBrightnessMapping() != null) {
-            LuxToBrightnessMapping mapping = autoBrightnessConfig.getLuxToBrightnessMapping();
-            final int size = mapping.getMap().getPoint().size();
-            mBrightnessLevels = new float[size];
-            mBrightnessLevelsLux = new float[size];
-            for (int i = 0; i < size; i++) {
-                float backlight = mapping.getMap().getPoint().get(i).getSecond().floatValue();
-                mBrightnessLevels[i] = mBacklightToBrightnessSpline.interpolate(backlight);
-                mBrightnessLevelsLux[i] = mapping.getMap().getPoint().get(i).getFirst()
-                        .floatValue();
-            }
-            if (size > 0 && mBrightnessLevelsLux[0] != 0) {
-                throw new IllegalArgumentException(
-                        "The first lux value in the display brightness mapping must be 0");
-            }
-        } else {
-            mBrightnessLevelsNits = getFloatArray(mContext.getResources()
-                    .obtainTypedArray(com.android.internal.R.array
-                            .config_autoBrightnessDisplayValuesNits), PowerManager
-                    .BRIGHTNESS_OFF_FLOAT);
-            mBrightnessLevelsLux = getLuxLevels(mContext.getResources()
-                    .getIntArray(com.android.internal.R.array
-                            .config_autoBrightnessLevels));
-        }
-    }
-
     private void loadAutoBrightnessAvailableFromConfigXml() {
         mAutoBrightnessAvailable = mContext.getResources().getBoolean(
                 R.bool.config_automatic_brightness_available);
@@ -2977,7 +2949,8 @@
     }
 
     private void loadAutoBrightnessConfigsFromConfigXml() {
-        loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/);
+        mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
+                /* autoBrightnessConfig= */ null, mBacklightToBrightnessSpline);
     }
 
     private void loadBrightnessChangeThresholdsFromXml() {
@@ -3347,7 +3320,12 @@
         return vals;
     }
 
-    private static float[] getLuxLevels(int[] lux) {
+    /**
+     * @param lux The lux array
+     * @return The lux array with 0 appended at the beginning - the first lux value should always
+     * be 0
+     */
+    public static float[] getLuxLevels(int[] lux) {
         // The first control point is implicit and always at 0 lux.
         float[] levels = new float[lux.length + 1];
         for (int i = 0; i < lux.length; i++) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 2ab15e6..e38d08f 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2880,7 +2880,9 @@
             final DisplayPowerControllerInterface displayPowerController =
                     mDisplayPowerControllers.get(displayId);
             if (displayPowerController != null) {
-                displayPowerController.setAutomaticScreenBrightnessMode(enabled);
+                displayPowerController.setAutomaticScreenBrightnessMode(enabled
+                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE
+                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT);
             }
         }
     }
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 1bd556b..4e341a9 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -56,6 +56,15 @@
         }
     }
 
+    @Override
+    public boolean blockScreenOn(Runnable unblocker) {
+        if (mDisplayOffloader == null) {
+            return false;
+        }
+        mDisplayOffloader.onBlockingScreenOn(unblocker);
+        return true;
+    }
+
     /**
      * Start the offload session. The method returns if the session is already active.
      * @return Whether the session was started successfully
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 2cca72e..06e5f99 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -16,6 +16,9 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
 import android.annotation.NonNull;
@@ -53,7 +56,6 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.FloatProperty;
-import android.util.Log;
 import android.util.MathUtils;
 import android.util.MutableFloat;
 import android.util.MutableInt;
@@ -498,15 +500,6 @@
     private Sensor mLightSensor;
     private Sensor mScreenOffBrightnessSensor;
 
-    // The mappers between ambient lux, display backlight values, and display brightness.
-    // We will switch between the idle mapper and active mapper in AutomaticBrightnessController.
-    // Mapper used for active (normal) screen brightness mode
-    @Nullable
-    private BrightnessMappingStrategy mInteractiveModeBrightnessMapper;
-    // Mapper used for idle screen brightness mode
-    @Nullable
-    private BrightnessMappingStrategy mIdleModeBrightnessMapper;
-
     // The current brightness configuration.
     @Nullable
     private BrightnessConfiguration mBrightnessConfiguration;
@@ -609,7 +602,7 @@
             BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
             Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata,
             boolean bootCompleted, DisplayManagerFlags flags) {
-
+        mFlags = flags;
         mInjector = injector != null ? injector : new Injector();
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
@@ -742,7 +735,7 @@
             mCdsi = null;
         }
 
-        setUpAutoBrightness(resources, handler);
+        setUpAutoBrightness(context, handler);
 
         mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic()
                 && !resources.getBoolean(
@@ -767,8 +760,6 @@
         mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
 
         mBootCompleted = bootCompleted;
-
-        mFlags = flags;
     }
 
     private void applyReduceBrightColorsSplineAdjustment() {
@@ -780,13 +771,6 @@
         if (mAutomaticBrightnessController == null) {
             return;
         }
-        if ((!mAutomaticBrightnessController.isInIdleMode()
-                && mInteractiveModeBrightnessMapper == null)
-                || (mAutomaticBrightnessController.isInIdleMode()
-                && mIdleModeBrightnessMapper == null)) {
-            Log.w(mTag, "No brightness mapping available to recalculate splines for this mode");
-            return;
-        }
 
         float[] adjustedNits = new float[mNitsRange.length];
         for (int i = 0; i < mNitsRange.length; i++) {
@@ -848,10 +832,10 @@
     public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
             boolean slowChange) {
         mBrightnessRangeController.onAmbientLuxChange(ambientLux);
-        if (nits < 0) {
+        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
             mBrightnessToFollow = leadDisplayBrightness;
         } else {
-            float brightness = convertToFloatScale(nits);
+            float brightness = getBrightnessFromNits(nits);
             if (isValidBrightnessValue(brightness)) {
                 mBrightnessToFollow = brightness;
             } else {
@@ -876,7 +860,7 @@
         synchronized (mLock) {
             mDisplayBrightnessFollowers.remove(follower.getDisplayId());
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
                     /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
     }
@@ -886,7 +870,7 @@
         for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
             DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
                     /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
         mDisplayBrightnessFollowers.clear();
@@ -1091,7 +1075,7 @@
         loadBrightnessRampRates();
         loadProximitySensor();
         loadNitsRange(mContext.getResources());
-        setUpAutoBrightness(mContext.getResources(), mHandler);
+        setUpAutoBrightness(mContext, mHandler);
         reloadReduceBrightColours();
         setAnimatorRampSpeeds(/* isIdleMode= */ false);
         mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
@@ -1161,32 +1145,41 @@
         handleBrightnessModeChange();
     }
 
-    private void setUpAutoBrightness(Resources resources, Handler handler) {
+    private void setUpAutoBrightness(Context context, Handler handler) {
         mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable();
 
         if (!mUseSoftwareAutoBrightnessConfig) {
             return;
         }
 
-        float userLux = BrightnessMappingStrategy.NO_USER_LUX;
-        float userNits = -1;
-        if (mInteractiveModeBrightnessMapper != null) {
-            userLux = mInteractiveModeBrightnessMapper.getUserLux();
-            float userBrightness = mInteractiveModeBrightnessMapper.getUserBrightness();
-            userNits = mInteractiveModeBrightnessMapper.convertToNits(userBrightness);
-        }
+        SparseArray<BrightnessMappingStrategy> brightnessMappers = new SparseArray<>();
 
-        final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
+        BrightnessMappingStrategy defaultModeBrightnessMapper =
+                mInjector.getDefaultModeBrightnessMapper(context, mDisplayDeviceConfig,
+                        mDisplayWhiteBalanceController);
+        brightnessMappers.append(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                defaultModeBrightnessMapper);
+
+        final boolean isIdleScreenBrightnessEnabled = context.getResources().getBoolean(
                 R.bool.config_enableIdleScreenBrightnessMode);
-        mInteractiveModeBrightnessMapper = mInjector.getInteractiveModeBrightnessMapper(resources,
-                mDisplayDeviceConfig, mDisplayWhiteBalanceController);
         if (isIdleScreenBrightnessEnabled) {
-            mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources,
-                    mDisplayDeviceConfig, mDisplayWhiteBalanceController);
+            BrightnessMappingStrategy idleModeBrightnessMapper =
+                    BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
+                            AUTO_BRIGHTNESS_MODE_IDLE, mDisplayWhiteBalanceController);
+            if (idleModeBrightnessMapper != null) {
+                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE, idleModeBrightnessMapper);
+            }
         }
 
-        if (mInteractiveModeBrightnessMapper != null) {
-            final float dozeScaleFactor = resources.getFraction(
+        float userLux = BrightnessMappingStrategy.INVALID_LUX;
+        float userNits = BrightnessMappingStrategy.INVALID_NITS;
+        if (mAutomaticBrightnessController != null) {
+            userLux = mAutomaticBrightnessController.getUserLux();
+            userNits = mAutomaticBrightnessController.getUserNits();
+        }
+
+        if (defaultModeBrightnessMapper != null) {
+            final float dozeScaleFactor = context.getResources().getFraction(
                     com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
@@ -1270,14 +1263,14 @@
                     .getAutoBrightnessBrighteningLightDebounceIdle();
             long darkeningLightDebounceIdle = mDisplayDeviceConfig
                     .getAutoBrightnessDarkeningLightDebounceIdle();
-            boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
+            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
                     com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
 
-            int lightSensorWarmUpTimeConfig = resources.getInteger(
+            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
                     com.android.internal.R.integer.config_lightSensorWarmupTime);
-            int lightSensorRate = resources.getInteger(
+            int lightSensorRate = context.getResources().getInteger(
                     com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = resources.getInteger(
+            int initialLightSensorRate = context.getResources().getInteger(
                     com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
             if (initialLightSensorRate == -1) {
                 initialLightSensorRate = lightSensorRate;
@@ -1297,25 +1290,17 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
-            float userBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
-            if (userNits >= 0) {
-                userBrightness = mInteractiveModeBrightnessMapper.convertToFloatScale(userNits);
-                if (Float.isNaN(userBrightness)) {
-                    userBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
-                }
-            }
             mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
                     this, handler.getLooper(), mSensorManager, mLightSensor,
-                    mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig,
-                    PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor,
-                    lightSensorRate, initialLightSensorRate, brighteningLightDebounce,
-                    darkeningLightDebounce, brighteningLightDebounceIdle,
-                    darkeningLightDebounceIdle, autoBrightnessResetAmbientLuxAfterWarmUp,
-                    ambientBrightnessThresholds, screenBrightnessThresholds,
-                    ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext,
-                    mBrightnessRangeController, mBrightnessThrottler, mIdleModeBrightnessMapper,
-                    mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);
+                    brightnessMappers, lightSensorWarmUpTimeConfig, PowerManager.BRIGHTNESS_MIN,
+                    PowerManager.BRIGHTNESS_MAX, dozeScaleFactor, lightSensorRate,
+                    initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
+                    brighteningLightDebounceIdle, darkeningLightDebounceIdle,
+                    autoBrightnessResetAmbientLuxAfterWarmUp, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
+                    mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
 
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
@@ -1334,7 +1319,7 @@
                                 mHandler,
                                 SystemClock::uptimeMillis,
                                 sensorValueToLux,
-                                mInteractiveModeBrightnessMapper);
+                                defaultModeBrightnessMapper);
             }
         } else {
             mUseSoftwareAutoBrightnessConfig = false;
@@ -1377,13 +1362,11 @@
     }
 
     @Override
-    public void setAutomaticScreenBrightnessMode(boolean isIdle) {
+    public void setAutomaticScreenBrightnessMode(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+        boolean isIdle = mode == AUTO_BRIGHTNESS_MODE_IDLE;
         if (mAutomaticBrightnessController != null) {
-            if (isIdle) {
-                mAutomaticBrightnessController.switchToIdleMode();
-            } else {
-                mAutomaticBrightnessController.switchToInteractiveScreenBrightnessMode();
-            }
+            mAutomaticBrightnessController.switchMode(mode);
             setAnimatorRampSpeeds(isIdle);
         }
 
@@ -2810,7 +2793,7 @@
             float brightnessNitsForDefaultDisplay =
                     mBrightnessSetting.getBrightnessNitsForDefaultDisplay();
             if (brightnessNitsForDefaultDisplay >= 0) {
-                float brightnessForDefaultDisplay = convertToFloatScale(
+                float brightnessForDefaultDisplay = getBrightnessFromNits(
                         brightnessNitsForDefaultDisplay);
                 if (isValidBrightnessValue(brightnessForDefaultDisplay)) {
                     mBrightnessSetting.setBrightness(brightnessForDefaultDisplay);
@@ -2944,23 +2927,23 @@
 
     private float convertToNits(float brightness) {
         if (mAutomaticBrightnessController == null) {
-            return -1f;
+            return BrightnessMappingStrategy.INVALID_NITS;
         }
         return mAutomaticBrightnessController.convertToNits(brightness);
     }
 
     private float convertToAdjustedNits(float brightness) {
         if (mAutomaticBrightnessController == null) {
-            return -1f;
+            return BrightnessMappingStrategy.INVALID_NITS;
         }
         return mAutomaticBrightnessController.convertToAdjustedNits(brightness);
     }
 
-    private float convertToFloatScale(float nits) {
+    private float getBrightnessFromNits(float nits) {
         if (mAutomaticBrightnessController == null) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
-        return mAutomaticBrightnessController.convertToFloatScale(nits);
+        return mAutomaticBrightnessController.getBrightnessFromNits(nits);
     }
 
     @GuardedBy("mLock")
@@ -3635,7 +3618,7 @@
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
-                BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
                 int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
                 float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
                 long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
@@ -3645,27 +3628,26 @@
                 HysteresisLevels screenBrightnessThresholds,
                 HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-                BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler,
-                BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userBrightness) {
+                BrightnessRangeController brightnessModeController,
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits) {
             return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
-                    interactiveModeBrightnessMapper, lightSensorWarmUpTime, brightnessMin,
+                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
                     brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
                     brighteningLightDebounceConfig, darkeningLightDebounceConfig,
                     brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
                     resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
-                    screenBrightnessThresholdsIdle, context, brightnessRangeController,
-                    brightnessThrottler, idleModeBrightnessMapper, ambientLightHorizonShort,
-                    ambientLightHorizonLong, userLux, userBrightness);
+                    screenBrightnessThresholdsIdle, context, brightnessModeController,
+                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                    userNits);
         }
 
-        BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
                 DisplayDeviceConfig displayDeviceConfig,
                 DisplayWhiteBalanceController displayWhiteBalanceController) {
-            return BrightnessMappingStrategy.create(resources,
-                    displayDeviceConfig, displayWhiteBalanceController);
+            return BrightnessMappingStrategy.create(context, displayDeviceConfig,
+                    AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
         }
 
         HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 810ac08..519224a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -16,6 +16,11 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString;
+
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
 import android.annotation.Nullable;
@@ -52,7 +57,6 @@
 import android.provider.Settings;
 import android.util.FloatProperty;
 import android.util.IndentingPrintWriter;
-import android.util.Log;
 import android.util.MathUtils;
 import android.util.MutableFloat;
 import android.util.MutableInt;
@@ -124,6 +128,8 @@
     // To enable these logs, run:
     // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+    private static final String SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME =
+            "Screen on blocked by displayoffload";
 
     // If true, uses the color fade on animation.
     // We might want to turn this off if we cannot get a guarantee that the screen
@@ -153,6 +159,7 @@
     private static final int MSG_SET_DWBC_COLOR_OVERRIDE = 15;
     private static final int MSG_SET_DWBC_LOGGING_ENABLED = 16;
     private static final int MSG_SET_BRIGHTNESS_FROM_OFFLOAD = 17;
+    private static final int MSG_OFFLOADING_SCREEN_ON_UNBLOCKED = 18;
 
 
 
@@ -337,6 +344,7 @@
     // we are waiting for a callback to release it and unblock the screen.
     private ScreenOnUnblocker mPendingScreenOnUnblocker;
     private ScreenOffUnblocker mPendingScreenOffUnblocker;
+    private Runnable mPendingScreenOnUnblockerByDisplayOffload;
 
     // True if we were in the process of turning off the screen.
     // This allows us to recover more gracefully from situations where we abort
@@ -346,10 +354,15 @@
     // The elapsed real time when the screen on was blocked.
     private long mScreenOnBlockStartRealTime;
     private long mScreenOffBlockStartRealTime;
+    private long mScreenOnBlockByDisplayOffloadStartRealTime;
 
     // Screen state we reported to policy. Must be one of REPORTED_TO_POLICY_* fields.
     private int mReportedScreenStateToPolicy = REPORTED_TO_POLICY_UNREPORTED;
 
+    // Used to deduplicate the displayoffload blocking screen on logic. One block per turning on.
+    // This value is reset when screen on is reported or the blocking is cancelled.
+    private boolean mScreenTurningOnWasBlockedByDisplayOffload;
+
     // If the last recorded screen state was dozing or not.
     private boolean mDozing;
 
@@ -442,15 +455,6 @@
     private Sensor mLightSensor;
     private Sensor mScreenOffBrightnessSensor;
 
-    // The mappers between ambient lux, display backlight values, and display brightness.
-    // We will switch between the idle mapper and active mapper in AutomaticBrightnessController.
-    // Mapper used for active (normal) screen brightness mode
-    @Nullable
-    private BrightnessMappingStrategy mInteractiveModeBrightnessMapper;
-    // Mapper used for idle screen brightness mode
-    @Nullable
-    private BrightnessMappingStrategy mIdleModeBrightnessMapper;
-
     private boolean mIsRbcActive;
 
     // Animators.
@@ -479,7 +483,7 @@
     private boolean mBootCompleted;
     private final DisplayManagerFlags mFlags;
 
-    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+    private DisplayOffloadSession mDisplayOffloadSession;
 
     /**
      * Creates the display power controller.
@@ -490,7 +494,7 @@
             BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
             Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata,
             boolean bootCompleted, DisplayManagerFlags flags) {
-
+        mFlags = flags;
         mInjector = injector != null ? injector : new Injector();
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
@@ -621,7 +625,7 @@
             mCdsi = null;
         }
 
-        setUpAutoBrightness(resources, handler);
+        setUpAutoBrightness(context, handler);
 
         mColorFadeEnabled = mInjector.isColorFadeEnabled()
                 && !resources.getBoolean(
@@ -636,7 +640,6 @@
                 R.bool.config_displayBrightnessBucketsInDoze);
 
         mBootCompleted = bootCompleted;
-        mFlags = flags;
     }
 
     private void applyReduceBrightColorsSplineAdjustment() {
@@ -648,13 +651,6 @@
         if (mAutomaticBrightnessController == null) {
             return;
         }
-        if ((!mAutomaticBrightnessController.isInIdleMode()
-                && mInteractiveModeBrightnessMapper == null)
-                || (mAutomaticBrightnessController.isInIdleMode()
-                && mIdleModeBrightnessMapper == null)) {
-            Log.w(mTag, "No brightness mapping available to recalculate splines for this mode");
-            return;
-        }
 
         float[] adjustedNits = new float[mNitsRange.length];
         for (int i = 0; i < mNitsRange.length; i++) {
@@ -787,6 +783,10 @@
 
     @Override
     public void setDisplayOffloadSession(DisplayOffloadSession session) {
+        if (session == mDisplayOffloadSession) {
+            return;
+        }
+        unblockScreenOnByDisplayOffload();
         mDisplayOffloadSession = session;
     }
 
@@ -906,7 +906,7 @@
         // updated here.
         loadBrightnessRampRates();
         loadNitsRange(mContext.getResources());
-        setUpAutoBrightness(mContext.getResources(), mHandler);
+        setUpAutoBrightness(mContext, mHandler);
         reloadReduceBrightColours();
         setAnimatorRampSpeeds(/* isIdleMode= */ false);
 
@@ -977,35 +977,58 @@
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+        if (mFlags.areAutoBrightnessModesEnabled()) {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_ALS),
+                    /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_CURRENT);
+        }
         handleBrightnessModeChange();
     }
 
-    private void setUpAutoBrightness(Resources resources, Handler handler) {
+    private void setUpAutoBrightness(Context context, Handler handler) {
         mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable();
 
         if (!mUseSoftwareAutoBrightnessConfig) {
             return;
         }
 
-        float userLux = BrightnessMappingStrategy.NO_USER_LUX;
-        float userNits = -1;
-        if (mInteractiveModeBrightnessMapper != null) {
-            userLux = mInteractiveModeBrightnessMapper.getUserLux();
-            float userBrightness = mInteractiveModeBrightnessMapper.getUserBrightness();
-            userNits = mInteractiveModeBrightnessMapper.convertToNits(userBrightness);
-        }
+        SparseArray<BrightnessMappingStrategy> brightnessMappers = new SparseArray<>();
 
-        final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
+        BrightnessMappingStrategy defaultModeBrightnessMapper =
+                mInjector.getDefaultModeBrightnessMapper(context, mDisplayDeviceConfig,
+                        mDisplayWhiteBalanceController);
+        brightnessMappers.append(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                defaultModeBrightnessMapper);
+
+        final boolean isIdleScreenBrightnessEnabled = context.getResources().getBoolean(
                 R.bool.config_enableIdleScreenBrightnessMode);
-        mInteractiveModeBrightnessMapper = mInjector.getInteractiveModeBrightnessMapper(resources,
-                mDisplayDeviceConfig, mDisplayWhiteBalanceController);
         if (isIdleScreenBrightnessEnabled) {
-            mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources,
-                    mDisplayDeviceConfig, mDisplayWhiteBalanceController);
+            BrightnessMappingStrategy idleModeBrightnessMapper =
+                    BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
+                            AUTO_BRIGHTNESS_MODE_IDLE,
+                            mDisplayWhiteBalanceController);
+            if (idleModeBrightnessMapper != null) {
+                brightnessMappers.append(AUTO_BRIGHTNESS_MODE_IDLE,
+                        idleModeBrightnessMapper);
+            }
         }
 
-        if (mInteractiveModeBrightnessMapper != null) {
-            final float dozeScaleFactor = resources.getFraction(
+        BrightnessMappingStrategy dozeModeBrightnessMapper =
+                BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
+                        AUTO_BRIGHTNESS_MODE_DOZE, mDisplayWhiteBalanceController);
+        if (mFlags.areAutoBrightnessModesEnabled() && dozeModeBrightnessMapper != null) {
+            brightnessMappers.put(AUTO_BRIGHTNESS_MODE_DOZE, dozeModeBrightnessMapper);
+        }
+
+        float userLux = BrightnessMappingStrategy.INVALID_LUX;
+        float userNits = BrightnessMappingStrategy.INVALID_NITS;
+        if (mAutomaticBrightnessController != null) {
+            userLux = mAutomaticBrightnessController.getUserLux();
+            userNits = mAutomaticBrightnessController.getUserNits();
+        }
+
+        if (defaultModeBrightnessMapper != null) {
+            final float dozeScaleFactor = context.getResources().getFraction(
                     R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
@@ -1089,14 +1112,14 @@
                     .getAutoBrightnessBrighteningLightDebounceIdle();
             long darkeningLightDebounceIdle = mDisplayDeviceConfig
                     .getAutoBrightnessDarkeningLightDebounceIdle();
-            boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
+            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
                     R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
 
-            int lightSensorWarmUpTimeConfig = resources.getInteger(
+            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
                     R.integer.config_lightSensorWarmupTime);
-            int lightSensorRate = resources.getInteger(
+            int lightSensorRate = context.getResources().getInteger(
                     R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = resources.getInteger(
+            int initialLightSensorRate = context.getResources().getInteger(
                     R.integer.config_autoBrightnessInitialLightSensorRate);
             if (initialLightSensorRate == -1) {
                 initialLightSensorRate = lightSensorRate;
@@ -1116,25 +1139,17 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
-            float userBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
-            if (userNits >= 0) {
-                userBrightness = mInteractiveModeBrightnessMapper.convertToFloatScale(userNits);
-                if (Float.isNaN(userBrightness)) {
-                    userBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
-                }
-            }
             mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
                     this, handler.getLooper(), mSensorManager, mLightSensor,
-                    mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig,
-                    PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor,
-                    lightSensorRate, initialLightSensorRate, brighteningLightDebounce,
-                    darkeningLightDebounce, brighteningLightDebounceIdle,
-                    darkeningLightDebounceIdle, autoBrightnessResetAmbientLuxAfterWarmUp,
-                    ambientBrightnessThresholds, screenBrightnessThresholds,
-                    ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext,
-                    mBrightnessRangeController, mBrightnessThrottler, mIdleModeBrightnessMapper,
-                    mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);
+                    brightnessMappers, lightSensorWarmUpTimeConfig, PowerManager.BRIGHTNESS_MIN,
+                    PowerManager.BRIGHTNESS_MAX, dozeScaleFactor, lightSensorRate,
+                    initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
+                    brighteningLightDebounceIdle, darkeningLightDebounceIdle,
+                    autoBrightnessResetAmbientLuxAfterWarmUp, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
+                    mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
             mDisplayBrightnessController.setAutomaticBrightnessController(
                     mAutomaticBrightnessController);
 
@@ -1157,7 +1172,7 @@
                                 mHandler,
                                 SystemClock::uptimeMillis,
                                 sensorValueToLux,
-                                mInteractiveModeBrightnessMapper);
+                                defaultModeBrightnessMapper);
             }
         } else {
             mUseSoftwareAutoBrightnessConfig = false;
@@ -1200,13 +1215,11 @@
     }
 
     @Override
-    public void setAutomaticScreenBrightnessMode(boolean isIdle) {
+    public void setAutomaticScreenBrightnessMode(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+        boolean isIdle = mode == AUTO_BRIGHTNESS_MODE_IDLE;
         if (mAutomaticBrightnessController != null) {
-            if (isIdle) {
-                mAutomaticBrightnessController.switchToIdleMode();
-            } else {
-                mAutomaticBrightnessController.switchToInteractiveScreenBrightnessMode();
-            }
+            mAutomaticBrightnessController.switchMode(mode);
             setAnimatorRampSpeeds(isIdle);
         }
         Message msg = mHandler.obtainMessage();
@@ -1350,6 +1363,13 @@
         animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
 
+        // Switch to doze auto-brightness mode if needed
+        if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null
+                && !mAutomaticBrightnessController.isInIdleMode()) {
+            setAutomaticScreenBrightnessMode(Display.isDozeState(state)
+                    ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
+        }
+
         final boolean userSetBrightnessChanged = mDisplayBrightnessController
                 .updateUserSetScreenBrightness();
 
@@ -1749,6 +1769,7 @@
         // reporting the display is ready because we only need to ensure the screen is in the
         // right power state even as it continues to converge on the desired brightness.
         final boolean ready = mPendingScreenOnUnblocker == null
+                && mPendingScreenOnUnblockerByDisplayOffload == null
                 && (!mColorFadeEnabled || (!mColorFadeOnAnimator.isStarted()
                         && !mColorFadeOffAnimator.isStarted()))
                 && mPowerState.waitUntilClean(mCleanListener);
@@ -1997,15 +2018,69 @@
         }
     }
 
+    private void blockScreenOnByDisplayOffload(DisplayOffloadSession displayOffloadSession) {
+        if (mPendingScreenOnUnblockerByDisplayOffload != null || displayOffloadSession == null) {
+            return;
+        }
+        mScreenTurningOnWasBlockedByDisplayOffload = true;
+
+        Trace.asyncTraceBegin(
+                Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
+        mScreenOnBlockByDisplayOffloadStartRealTime = SystemClock.elapsedRealtime();
+
+        mPendingScreenOnUnblockerByDisplayOffload =
+                () -> onDisplayOffloadUnblockScreenOn(displayOffloadSession);
+        if (!displayOffloadSession.blockScreenOn(mPendingScreenOnUnblockerByDisplayOffload)) {
+            mPendingScreenOnUnblockerByDisplayOffload = null;
+            long delay =
+                    SystemClock.elapsedRealtime() - mScreenOnBlockByDisplayOffloadStartRealTime;
+            Slog.w(mTag, "Tried blocking screen on for offloading but failed. So, end trace after "
+                    + delay + " ms.");
+            Trace.asyncTraceEnd(
+                    Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
+            return;
+        }
+        Slog.i(mTag, "Blocking screen on for offloading.");
+    }
+
+    private void onDisplayOffloadUnblockScreenOn(DisplayOffloadSession displayOffloadSession) {
+        Message msg = mHandler.obtainMessage(MSG_OFFLOADING_SCREEN_ON_UNBLOCKED,
+                displayOffloadSession);
+        mHandler.sendMessage(msg);
+    }
+
+    private void unblockScreenOnByDisplayOffload() {
+        if (mPendingScreenOnUnblockerByDisplayOffload == null) {
+            return;
+        }
+        mPendingScreenOnUnblockerByDisplayOffload = null;
+        long delay = SystemClock.elapsedRealtime() - mScreenOnBlockByDisplayOffloadStartRealTime;
+        Slog.i(mTag, "Unblocked screen on for offloading after " + delay + " ms");
+        Trace.asyncTraceEnd(
+                Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
+    }
+
     private boolean setScreenState(int state) {
         return setScreenState(state, false /*reportOnly*/);
     }
 
     private boolean setScreenState(int state, boolean reportOnly) {
         final boolean isOff = (state == Display.STATE_OFF);
+        final boolean isOn = (state == Display.STATE_ON);
+        final boolean changed = mPowerState.getScreenState() != state;
 
-        if (mPowerState.getScreenState() != state
-                || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
+        // If the screen is turning on, give displayoffload a chance to do something before the
+        // screen actually turns on.
+        // TODO(b/316941732): add tests for this displayoffload screen-on blocker.
+        if (isOn && changed && !mScreenTurningOnWasBlockedByDisplayOffload) {
+            blockScreenOnByDisplayOffload(mDisplayOffloadSession);
+        } else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) {
+            // No longer turning screen on, so unblock previous screen on blocking immediately.
+            unblockScreenOnByDisplayOffload();
+            mScreenTurningOnWasBlockedByDisplayOffload = false;
+        }
+
+        if (changed || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
             // If we are trying to turn screen off, give policy a chance to do something before we
             // actually turn the screen off.
             if (isOff && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
@@ -2021,8 +2096,9 @@
                 }
             }
 
-            if (!reportOnly && mPowerState.getScreenState() != state
-                    && readyToUpdateDisplayState()) {
+            if (!reportOnly && changed && readyToUpdateDisplayState()
+                    && mPendingScreenOffUnblocker == null
+                    && mPendingScreenOnUnblockerByDisplayOffload == null) {
                 Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
 
                 String propertyKey = "debug.tracing.screen_state";
@@ -2074,12 +2150,16 @@
         }
 
         // Return true if the screen isn't blocked.
-        return mPendingScreenOnUnblocker == null;
+        return mPendingScreenOnUnblocker == null
+                && mPendingScreenOnUnblockerByDisplayOffload == null;
     }
 
     private void setReportedScreenState(int state) {
         Trace.traceCounter(Trace.TRACE_TAG_POWER, "ReportedScreenStateToPolicy", state);
         mReportedScreenStateToPolicy = state;
+        if (state == REPORTED_TO_POLICY_SCREEN_ON) {
+            mScreenTurningOnWasBlockedByDisplayOffload = false;
+        }
     }
 
     private void loadAmbientLightSensor() {
@@ -2351,10 +2431,10 @@
     public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
             boolean slowChange) {
         mBrightnessRangeController.onAmbientLuxChange(ambientLux);
-        if (nits < 0) {
+        if (nits == BrightnessMappingStrategy.INVALID_NITS) {
             mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
         } else {
-            float brightness = mDisplayBrightnessController.convertToFloatScale(nits);
+            float brightness = mDisplayBrightnessController.getBrightnessFromNits(nits);
             if (BrightnessUtils.isValidBrightnessValue(brightness)) {
                 mDisplayBrightnessController.setBrightnessToFollow(brightness, slowChange);
             } else {
@@ -2421,7 +2501,7 @@
         synchronized (mLock) {
             mDisplayBrightnessFollowers.remove(follower.getDisplayId());
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
                     /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
     }
@@ -2431,7 +2511,7 @@
         for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
             DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
-                    PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS,
                     /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
         mDisplayBrightnessFollowers.clear();
@@ -2827,6 +2907,12 @@
                         updatePowerState();
                     }
                     break;
+                case MSG_OFFLOADING_SCREEN_ON_UNBLOCKED:
+                    if (mDisplayOffloadSession == msg.obj) {
+                        unblockScreenOnByDisplayOffload();
+                        updatePowerState();
+                    }
+                    break;
                 case MSG_CONFIGURE_BRIGHTNESS:
                     BrightnessConfiguration brightnessConfiguration =
                             (BrightnessConfiguration) msg.obj;
@@ -2919,6 +3005,16 @@
         public void onChange(boolean selfChange, Uri uri) {
             if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) {
                 handleBrightnessModeChange();
+            } else if (uri.equals(Settings.System.getUriFor(
+                    Settings.System.SCREEN_BRIGHTNESS_FOR_ALS))) {
+                int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL,
+                        UserHandle.USER_CURRENT);
+                Slog.i(mTag, "Setting up auto-brightness for preset "
+                        + autoBrightnessPresetToString(preset));
+                setUpAutoBrightness(mContext, mHandler);
+                sendUpdatePowerState();
             } else {
                 handleSettingsChange(false /* userSwitch */);
             }
@@ -3012,7 +3108,7 @@
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
-                BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
                 int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
                 float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
                 long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
@@ -3023,26 +3119,25 @@
                 HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessModeController,
-                BrightnessThrottler brightnessThrottler,
-                BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userBrightness) {
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits) {
             return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
-                    interactiveModeBrightnessMapper, lightSensorWarmUpTime, brightnessMin,
+                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
                     brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
                     brighteningLightDebounceConfig, darkeningLightDebounceConfig,
                     brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
                     resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                     screenBrightnessThresholdsIdle, context, brightnessModeController,
-                    brightnessThrottler, idleModeBrightnessMapper, ambientLightHorizonShort,
-                    ambientLightHorizonLong, userLux, userBrightness);
+                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                    userNits);
         }
 
-        BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
                 DisplayDeviceConfig displayDeviceConfig,
                 DisplayWhiteBalanceController displayWhiteBalanceController) {
-            return BrightnessMappingStrategy.create(resources,
-                    displayDeviceConfig, displayWhiteBalanceController);
+            return BrightnessMappingStrategy.create(context, displayDeviceConfig,
+                    AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
         }
 
         HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 72079a4..c279184 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -69,9 +69,10 @@
 
     /**
      * Used to decide the associated AutomaticBrightnessController's BrightnessMode
-     * @param isIdle Flag which represents if the Idle BrightnessMode is to be set
+     * @param mode The auto-brightness mode
      */
-    void setAutomaticScreenBrightnessMode(boolean isIdle);
+    void setAutomaticScreenBrightnessMode(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode);
 
     /**
      * Used to enable/disable the logging of the WhileBalance associated entities
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index f994c05..bcf27b4 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -26,9 +26,12 @@
 import android.view.Choreographer;
 import android.view.Display;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
 import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
+import java.util.concurrent.Executor;
 
 /**
  * Controls the display power state.
@@ -75,10 +78,19 @@
 
     private Runnable mCleanListener;
 
+    private Executor mAsyncDestroyExecutor;
+
     private volatile boolean mStopped;
 
     DisplayPowerState(
             DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState) {
+        this(blanker, colorFade, displayId, displayState, BackgroundThread.getExecutor());
+    }
+
+    @VisibleForTesting
+    DisplayPowerState(
+            DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState,
+            Executor asyncDestroyExecutor) {
         mHandler = new Handler(true /*async*/);
         mChoreographer = Choreographer.getInstance();
         mBlanker = blanker;
@@ -86,6 +98,7 @@
         mPhotonicModulator = new PhotonicModulator();
         mPhotonicModulator.start();
         mDisplayId = displayId;
+        mAsyncDestroyExecutor = asyncDestroyExecutor;
 
         // At boot time, we don't know the screen's brightness,
         // so prepare to set it to a known state when the state is next applied.
@@ -321,7 +334,7 @@
         mStopped = true;
         mPhotonicModulator.interrupt();
         if (mColorFade != null) {
-            mColorFade.destroy();
+            mAsyncDestroyExecutor.execute(mColorFade::destroy);
         }
         mCleanListener = null;
         mHandler.removeCallbacksAndMessages(null);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 22898a6..25576ce 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -25,6 +25,7 @@
 import android.content.res.Resources;
 import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
 import android.hardware.sidekick.SidekickInternal;
+import android.media.MediaDrm;
 import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
@@ -242,6 +243,10 @@
         private SurfaceControl.DisplayMode mActiveSfDisplayMode;
         // The active display vsync period in SurfaceFlinger
         private float mActiveRenderFrameRate;
+        // The current HDCP level supported by the display, 0 indicates unset
+        // values are defined in hardware/interfaces/drm/aidl/android/hardware/drm/HdcpLevel.aidl
+        private int mConnectedHdcpLevel;
+
         private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides =
                 new DisplayEventReceiver.FrameRateOverride[0];
 
@@ -675,8 +680,9 @@
                 mInfo.yDpi = mActiveSfDisplayMode.yDpi;
                 mInfo.deviceProductInfo = mStaticDisplayInfo.deviceProductInfo;
 
-                // Assume that all built-in displays that have secure output (eg. HDCP) also
-                // support compositing from gralloc protected buffers.
+                if (mConnectedHdcpLevel != 0) {
+                    mStaticDisplayInfo.secure = mConnectedHdcpLevel >= MediaDrm.HDCP_V1;
+                }
                 if (mStaticDisplayInfo.secure) {
                     mInfo.flags = DisplayDeviceInfo.FLAG_SECURE
                             | DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS;
@@ -1093,6 +1099,12 @@
             }
         }
 
+        public void onHdcpLevelsChangedLocked(int connectedLevel, int maxLevel) {
+            if (updateHdcpLevelsLocked(connectedLevel, maxLevel)) {
+                updateDeviceInfoLocked();
+            }
+        }
+
         public boolean updateActiveModeLocked(int activeSfModeId, float renderFrameRate) {
             if (mActiveSfDisplayMode.id == activeSfModeId
                     && mActiveRenderFrameRate == renderFrameRate) {
@@ -1118,6 +1130,22 @@
             return true;
         }
 
+        public boolean updateHdcpLevelsLocked(int connectedLevel, int maxLevel) {
+            if (connectedLevel > maxLevel) {
+                Slog.w(TAG, "HDCP connected level: " + connectedLevel
+                        + " is larger than max level: " + maxLevel
+                        + ", ignoring request.");
+                return false;
+            }
+
+            if (mConnectedHdcpLevel == connectedLevel) {
+                return false;
+            }
+
+            mConnectedHdcpLevel = connectedLevel;
+            return true;
+        }
+
         public void requestColorModeLocked(int colorMode) {
             if (mActiveColorMode == colorMode) {
                 return;
@@ -1387,6 +1415,7 @@
                 long renderPeriod);
         void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
                 DisplayEventReceiver.FrameRateOverride[] overrides);
+        void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel);
 
     }
 
@@ -1420,6 +1449,11 @@
                 DisplayEventReceiver.FrameRateOverride[] overrides) {
             mListener.onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
         }
+
+        @Override
+        public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) {
+            mListener.onHdcpLevelsChanged(physicalDisplayId, connectedLevel, maxLevel);
+        }
     }
 
     private final class LocalDisplayEventListener implements DisplayEventListener {
@@ -1489,6 +1523,26 @@
                 device.onFrameRateOverridesChanged(overrides);
             }
         }
+
+        @Override
+        public void onHdcpLevelsChanged(long physicalDisplayId, int connectedLevel, int maxLevel) {
+            if (DEBUG) {
+                Slog.d(TAG, "onHdcpLevelsChanged(physicalDisplayId=" + physicalDisplayId
+                        + ", connectedLevel=" + connectedLevel + ", maxLevel=" + maxLevel + ")");
+            }
+            synchronized (getSyncRoot()) {
+                LocalDisplayDevice device = mDevices.get(physicalDisplayId);
+                if (device == null) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Received hdcp levels change for unhandled physical display: "
+                                + "physicalDisplayId=" + physicalDisplayId);
+                    }
+                    return;
+                }
+
+                device.onHdcpLevelsChangedLocked(connectedLevel, maxLevel);
+            }
+        }
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index 617befb..3bb7986 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -27,6 +27,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.AutomaticBrightnessController;
+import com.android.server.display.BrightnessMappingStrategy;
 import com.android.server.display.BrightnessSetting;
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
@@ -359,11 +360,12 @@
      * passing the brightness value to follower displays.
      *
      * @param brightness The float scale value
-     * @return The nit value or -1f if no conversion is possible.
+     * @return The nit value or {@link BrightnessMappingStrategy.INVALID_NITS} if no conversion is
+     * possible.
      */
     public float convertToNits(float brightness) {
         if (mAutomaticBrightnessController == null) {
-            return -1f;
+            return BrightnessMappingStrategy.INVALID_NITS;
         }
         return mAutomaticBrightnessController.convertToNits(brightness);
     }
@@ -374,11 +376,12 @@
      * {@link com.android.server.display.BrightnessTracker}.
      *
      * @param brightness The float scale value
-     * @return The nit value or -1f if no conversion is possible.
+     * @return The nit value or {@link BrightnessMappingStrategy.INVALID_NITS} if no conversion is
+     * possible.
      */
     public float convertToAdjustedNits(float brightness) {
         if (mAutomaticBrightnessController == null) {
-            return -1f;
+            return BrightnessMappingStrategy.INVALID_NITS;
         }
         return mAutomaticBrightnessController.convertToAdjustedNits(brightness);
     }
@@ -391,11 +394,11 @@
      * @return The float scale value or {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if no
      * conversion is possible.
      */
-    public float convertToFloatScale(float nits) {
+    public float getBrightnessFromNits(float nits) {
         if (mAutomaticBrightnessController == null) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
-        return mAutomaticBrightnessController.convertToFloatScale(nits);
+        return mAutomaticBrightnessController.getBrightnessFromNits(nits);
     }
 
     /**
@@ -497,7 +500,7 @@
             float brightnessNitsForDefaultDisplay =
                     mBrightnessSetting.getBrightnessNitsForDefaultDisplay();
             if (brightnessNitsForDefaultDisplay >= 0) {
-                float brightnessForDefaultDisplay = convertToFloatScale(
+                float brightnessForDefaultDisplay = getBrightnessFromNits(
                         brightnessNitsForDefaultDisplay);
                 if (BrightnessUtils.isValidBrightnessValue(brightnessForDefaultDisplay)) {
                     mBrightnessSetting.setBrightness(brightnessForDefaultDisplay);
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
index 200d88a..01a8d360a 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -104,8 +104,7 @@
     public void resetHdrConfig(HdrBrightnessData data, int width, int height,
             float minimumHdrPercentOfScreen, IBinder displayToken) {
         mHdrBrightnessData = data;
-        mHdrListener.mHdrMinPixels = minimumHdrPercentOfScreen <= 0 ? -1
-                : (float) (width * height) * minimumHdrPercentOfScreen;
+        mHdrListener.mHdrMinPixels = (float) (width * height) * minimumHdrPercentOfScreen;
         if (displayToken != mRegisteredDisplayToken) { // token changed, resubscribe
             if (mRegisteredDisplayToken != null) { // previous token not null, unsubscribe
                 mHdrListener.unregister(mRegisteredDisplayToken);
@@ -115,7 +114,7 @@
             // new token not null and hdr min % of the screen is set, subscribe.
             // e.g. for virtual display, HBM data will be missing and HdrListener
             // should not be registered
-            if (displayToken != null && mHdrListener.mHdrMinPixels > 0) {
+            if (displayToken != null && mHdrListener.mHdrMinPixels >= 0) {
                 mHdrListener.register(displayToken);
                 mRegisteredDisplayToken = displayToken;
             }
@@ -140,8 +139,11 @@
         pw.println("  mDesiredMaxBrightness=" + mDesiredMaxBrightness);
         pw.println("  mTransitionRate=" + mTransitionRate);
         pw.println("  mDesiredTransitionRate=" + mDesiredTransitionRate);
+        pw.println("  mHdrVisible=" + mHdrVisible);
+        pw.println("  mHdrListener.mHdrMinPixels=" + mHdrListener.mHdrMinPixels);
         pw.println("  mHdrBrightnessData=" + (mHdrBrightnessData == null ? "null"
                 : mHdrBrightnessData.toString()));
+        pw.println("  mHdrListener registered=" + (mRegisteredDisplayToken != null));
         pw.println("  mAmbientLux=" + mAmbientLux);
     }
 
diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
new file mode 100644
index 0000000..6978686
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
@@ -0,0 +1,262 @@
+/*
+ * 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.config;
+
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.util.Spline;
+
+import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.AutomaticBrightnessController;
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.feature.DisplayManagerFlags;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides a mapping between lux and brightness values in order to support auto-brightness.
+ */
+public class DisplayBrightnessMappingConfig {
+
+    private static final String DEFAULT_BRIGHTNESS_MAPPING_KEY =
+            AutoBrightnessModeName._default.getRawName() + "_"
+                    + AutoBrightnessSettingName.normal.getRawName();
+
+    /**
+     * Array of desired screen brightness in nits corresponding to the lux values
+     * in the mBrightnessLevelsLuxMap.get(DEFAULT_ID) array. The display brightness is defined as
+     * the measured brightness of an all-white image. The brightness values must be non-negative and
+     * non-decreasing. This must be overridden in platform specific overlays
+     */
+    private float[] mBrightnessLevelsNits;
+
+    /**
+     * Map of arrays of desired screen brightness corresponding to the lux values
+     * in mBrightnessLevelsLuxMap, indexed by the auto-brightness mode and the brightness preset.
+     * The brightness values must be non-negative and non-decreasing. They must be between
+     * {@link PowerManager.BRIGHTNESS_MIN} and {@link PowerManager.BRIGHTNESS_MAX}.
+     *
+     * The keys are a concatenation of the auto-brightness mode and the brightness preset separated
+     * by an underscore, e.g. default_normal, default_dim, default_bright, doze_normal, doze_dim,
+     * doze_bright.
+     *
+     * The presets are used on devices that allow users to choose from a set of predefined options
+     * in display auto-brightness settings.
+     */
+    private final Map<String, float[]> mBrightnessLevelsMap = new HashMap<>();
+
+    /**
+     * Map of arrays of light sensor lux values to define our levels for auto-brightness support,
+     * indexed by the auto-brightness mode and the brightness preset.
+     *
+     * The first lux value in every array is always 0.
+     *
+     * The control points must be strictly increasing. Each control point corresponds to an entry
+     * in the brightness values arrays. For example, if lux == luxLevels[1] (second element
+     * of the levels array) then the brightness will be determined by brightnessLevels[1] (second
+     * element of the brightness values array).
+     *
+     * Spline interpolation is used to determine the auto-brightness values for lux levels between
+     * these control points.
+     *
+     * The keys are a concatenation of the auto-brightness mode and the brightness preset separated
+     * by an underscore, e.g. default_normal, default_dim, default_bright, doze_normal, doze_dim,
+     * doze_bright.
+     *
+     * The presets are used on devices that allow users to choose from a set of predefined options
+     * in display auto-brightness settings.
+     */
+    private final Map<String, float[]> mBrightnessLevelsLuxMap = new HashMap<>();
+
+    /**
+     * Loads the auto-brightness display brightness mappings. Internally, this takes care of
+     * loading the value from the display config, and if not present, falls back to config.xml.
+     */
+    public DisplayBrightnessMappingConfig(Context context, DisplayManagerFlags flags,
+            AutoBrightness autoBrightnessConfig, Spline backlightToBrightnessSpline) {
+        if (flags.areAutoBrightnessModesEnabled() && autoBrightnessConfig != null
+                && autoBrightnessConfig.getLuxToBrightnessMapping() != null
+                && autoBrightnessConfig.getLuxToBrightnessMapping().size() > 0) {
+            for (LuxToBrightnessMapping mapping
+                    : autoBrightnessConfig.getLuxToBrightnessMapping()) {
+                final int size = mapping.getMap().getPoint().size();
+                float[] brightnessLevels = new float[size];
+                float[] brightnessLevelsLux = new float[size];
+                for (int i = 0; i < size; i++) {
+                    float backlight = mapping.getMap().getPoint().get(i).getSecond().floatValue();
+                    brightnessLevels[i] = backlightToBrightnessSpline.interpolate(backlight);
+                    brightnessLevelsLux[i] = mapping.getMap().getPoint().get(i).getFirst()
+                            .floatValue();
+                }
+                if (size == 0) {
+                    throw new IllegalArgumentException(
+                            "A display brightness mapping should not be empty");
+                }
+                if (brightnessLevelsLux[0] != 0) {
+                    throw new IllegalArgumentException(
+                            "The first lux value in the display brightness mapping must be 0");
+                }
+
+                String key = (mapping.getMode() == null ? "default" : mapping.getMode()) + "_"
+                        + (mapping.getSetting() == null ? "normal" : mapping.getSetting());
+                if (mBrightnessLevelsMap.containsKey(key)
+                        || mBrightnessLevelsLuxMap.containsKey(key)) {
+                    throw new IllegalArgumentException(
+                            "A display brightness mapping with key " + key + " already exists");
+                }
+                mBrightnessLevelsMap.put(key, brightnessLevels);
+                mBrightnessLevelsLuxMap.put(key, brightnessLevelsLux);
+            }
+        }
+
+        if (!mBrightnessLevelsMap.containsKey(DEFAULT_BRIGHTNESS_MAPPING_KEY)
+                || !mBrightnessLevelsLuxMap.containsKey(DEFAULT_BRIGHTNESS_MAPPING_KEY)) {
+            mBrightnessLevelsNits = DisplayDeviceConfig.getFloatArray(context.getResources()
+                    .obtainTypedArray(com.android.internal.R.array
+                            .config_autoBrightnessDisplayValuesNits), PowerManager
+                    .BRIGHTNESS_OFF_FLOAT);
+
+            float[] brightnessLevelsLux = DisplayDeviceConfig.getLuxLevels(context.getResources()
+                    .getIntArray(com.android.internal.R.array
+                            .config_autoBrightnessLevels));
+            mBrightnessLevelsLuxMap.put(DEFAULT_BRIGHTNESS_MAPPING_KEY, brightnessLevelsLux);
+
+            // Load the old configuration in the range [0, 255]. The values need to be normalized
+            // to the range [0, 1].
+            int[] brightnessLevels = context.getResources().getIntArray(
+                    com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
+            mBrightnessLevelsMap.put(DEFAULT_BRIGHTNESS_MAPPING_KEY,
+                    brightnessArrayIntToFloat(brightnessLevels, backlightToBrightnessSpline));
+        }
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @param preset The brightness preset. Presets are used on devices that allow users to choose
+     *               from a set of predefined options in display auto-brightness settings.
+     * @return The default auto-brightness brightening ambient lux levels for the specified mode
+     * and preset
+     */
+    public float[] getLuxArray(@AutomaticBrightnessController.AutomaticBrightnessMode int mode,
+            int preset) {
+        return mBrightnessLevelsLuxMap.get(
+                autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset));
+    }
+
+    /**
+     * @return Auto brightness brightening nits levels
+     */
+    public float[] getNitsArray() {
+        return mBrightnessLevelsNits;
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @param preset The brightness preset. Presets are used on devices that allow users to choose
+     *               from a set of predefined options in display auto-brightness settings.
+     * @return The default auto-brightness brightening levels for the specified mode and preset
+     */
+    public float[] getBrightnessArray(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) {
+        return mBrightnessLevelsMap.get(
+                autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset));
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder brightnessLevelsLuxMapString = new StringBuilder("{");
+        for (Map.Entry<String, float[]> entry : mBrightnessLevelsLuxMap.entrySet()) {
+            brightnessLevelsLuxMapString.append(entry.getKey()).append("=").append(
+                    Arrays.toString(entry.getValue())).append(", ");
+        }
+        if (brightnessLevelsLuxMapString.length() > 2) {
+            brightnessLevelsLuxMapString.delete(brightnessLevelsLuxMapString.length() - 2,
+                    brightnessLevelsLuxMapString.length());
+        }
+        brightnessLevelsLuxMapString.append("}");
+
+        StringBuilder brightnessLevelsMapString = new StringBuilder("{");
+        for (Map.Entry<String, float[]> entry : mBrightnessLevelsMap.entrySet()) {
+            brightnessLevelsMapString.append(entry.getKey()).append("=").append(
+                    Arrays.toString(entry.getValue())).append(", ");
+        }
+        if (brightnessLevelsMapString.length() > 2) {
+            brightnessLevelsMapString.delete(brightnessLevelsMapString.length() - 2,
+                    brightnessLevelsMapString.length());
+        }
+        brightnessLevelsMapString.append("}");
+
+        return "mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits)
+                + ", mBrightnessLevelsLuxMap= " + brightnessLevelsLuxMapString
+                + ", mBrightnessLevelsMap= " + brightnessLevelsMapString;
+    }
+
+    /**
+     * @param mode The auto-brightness mode
+     * @return The string representing the mode
+     */
+    public static String autoBrightnessModeToString(
+            @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+        switch (mode) {
+            case AUTO_BRIGHTNESS_MODE_DEFAULT -> {
+                return AutoBrightnessModeName._default.getRawName();
+            }
+            case AUTO_BRIGHTNESS_MODE_IDLE -> {
+                return AutoBrightnessModeName.idle.getRawName();
+            }
+            case AUTO_BRIGHTNESS_MODE_DOZE -> {
+                return AutoBrightnessModeName.doze.getRawName();
+            }
+            default -> throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
+        }
+    }
+
+    /**
+     * @param preset The brightness preset. Presets are used on devices that allow users to choose
+     *               from a set of predefined options in display auto-brightness settings.
+     * @return The string representing the preset
+     */
+    public static String autoBrightnessPresetToString(int preset) {
+        return switch (preset) {
+            case Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM ->
+                    AutoBrightnessSettingName.dim.getRawName();
+            case Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL ->
+                    AutoBrightnessSettingName.normal.getRawName();
+            case Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT ->
+                    AutoBrightnessSettingName.bright.getRawName();
+            default -> throw new IllegalArgumentException(
+                    "Unknown auto-brightness preset value: " + preset);
+        };
+    }
+
+    private float[] brightnessArrayIntToFloat(int[] brightnessInt,
+            Spline backlightToBrightnessSpline) {
+        float[] brightnessFloat = new float[brightnessInt.length];
+        for (int i = 0; i < brightnessInt.length; i++) {
+            brightnessFloat[i] = backlightToBrightnessSpline.interpolate(
+                    BrightnessSynchronizer.brightnessIntToFloat(brightnessInt[i]));
+        }
+        return brightnessFloat;
+    }
+}
diff --git a/services/core/java/com/android/server/flags/OWNERS b/services/core/java/com/android/server/flags/OWNERS
index 535a750..60ceb12 100644
--- a/services/core/java/com/android/server/flags/OWNERS
+++ b/services/core/java/com/android/server/flags/OWNERS
@@ -1 +1,2 @@
-per-file pinner.aconfig = edgararriaga@google.com
\ No newline at end of file
+per-file pinner.aconfig = edgararriaga@google.com
+per-file compaction.aconfig = edgararriaga@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/flags/compaction.aconfig b/services/core/java/com/android/server/flags/compaction.aconfig
new file mode 100644
index 0000000..58cc560
--- /dev/null
+++ b/services/core/java/com/android/server/flags/compaction.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.flags"
+
+flag {
+    name: "disable_system_compaction"
+    namespace: "system_performance"
+    description: "This flag controls if all processes compaction should happen during idle maintenance."
+    bug: "314328789"
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 64abb81..81204ef 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1314,7 +1314,6 @@
      */
     protected void disableDevice(
             boolean initiatedByCec, final PendingActionClearedCallback originalCallback) {
-        removeAction(AbsoluteVolumeAudioStatusAction.class);
         removeAction(SetAudioVolumeLevelDiscoveryAction.class);
         removeAction(ActiveSourceAction.class);
         removeAction(ResendCecCommandAction.class);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index b3aa351..70993ca 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -201,7 +201,8 @@
         if (WAKE_ON_HOTPLUG && connected) {
             mService.wakeUp();
         }
-        if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) {
+        HdmiPortInfo portInfo = mService.getPortInfo(portId);
+        if (portInfo != null && portInfo.getType() == HdmiPortInfo.PORT_OUTPUT) {
             mCecMessageCache.flushAll();
             if (!connected) {
                 if (isSystemAudioActivated()) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
index 7e8a2cc..6157402 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
@@ -92,7 +92,8 @@
     @ServiceThreadOnly
     void onHotplug(int portId, boolean connected) {
         assertRunOnServiceThread();
-        if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) {
+        HdmiPortInfo portInfo = mService.getPortInfo(portId);
+        if (portInfo != null && portInfo.getType() == HdmiPortInfo.PORT_OUTPUT) {
             mCecMessageCache.flushAll();
         }
         // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3.
@@ -307,7 +308,6 @@
     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
         removeAction(OneTouchPlayAction.class);
         removeAction(DevicePowerStatusAction.class);
-        removeAction(AbsoluteVolumeAudioStatusAction.class);
 
         super.disableDevice(initiatedByCec, callback);
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 5831b29..1cd267d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1336,7 +1336,6 @@
         removeAction(OneTouchRecordAction.class);
         removeAction(TimerRecordingAction.class);
         removeAction(NewDeviceAction.class);
-        removeAction(AbsoluteVolumeAudioStatusAction.class);
         // Remove pending actions.
         removeAction(RequestActiveSourceAction.class);
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index b3926fd..eaf754d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1783,8 +1783,9 @@
         // initPortInfo at hotplug event.
         mHdmiCecNetwork.initPortInfo();
 
+        HdmiPortInfo portInfo = getPortInfo(portId);
         if (connected && !isTvDevice()
-                && getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) {
+                && portInfo != null && portInfo.getType() == HdmiPortInfo.PORT_OUTPUT) {
             ArrayList<HdmiCecLocalDevice> localDevices = new ArrayList<>();
             for (int type : getCecLocalDeviceTypes()) {
                 HdmiCecLocalDevice localDevice = mHdmiCecNetwork.getLocalDevice(type);
@@ -3792,6 +3793,11 @@
                 }
             }
         });
+
+        // Make sure we switch away from absolute volume behavior (AVB) when entering standby.
+        // We do this because AVB should not be used unless AbsoluteVolumeAudioStatusAction exists,
+        // and the action cannot exist in standby because there are no local devices.
+        checkAndUpdateAbsoluteVolumeBehavior();
     }
 
     boolean canGoToStandby() {
@@ -4445,10 +4451,11 @@
      * This allows the volume level of the System Audio device to be tracked and set by Android.
      *
      * Absolute volume behavior requires the following conditions:
-     * 1. If the System Audio Device is an Audio System: System Audio Mode is active
-     * 2. All AVB-capable audio output devices are already using full/absolute volume behavior
-     * 3. CEC volume is enabled
-     * 4. The System Audio device supports the <Set Audio Volume Level> message
+     * 1. The device is not in standby or transient to standby
+     * 2. If the System Audio Device is an Audio System: System Audio Mode is active
+     * 3. All AVB-capable audio output devices are already using full/absolute volume behavior
+     * 4. CEC volume is enabled
+     * 5. The System Audio device supports the <Set Audio Volume Level> message
      *
      * This method enables adjust-only absolute volume behavior on TV panels when conditions
      * 1, 2, and 3 are met, but condition 4 is not. This allows TVs to track the volume level of
@@ -4464,10 +4471,16 @@
             return;
         }
 
+        // Condition 1: The device is not in standby or transient to standby
+        if (mPowerStatusController != null && isPowerStandbyOrTransient()) {
+            switchToFullVolumeBehavior();
+            return;
+        }
+
         HdmiCecLocalDevice localCecDevice;
         if (isTvDevice() && tv() != null) {
             localCecDevice = tv();
-            // Condition 1: TVs need System Audio Mode to be active
+            // Condition 2: TVs need System Audio Mode to be active
             // (Doesn't apply to Playback Devices, where if SAM isn't active, we assume the
             // TV is the System Audio Device instead.)
             if (!isSystemAudioActivated()) {
@@ -4484,7 +4497,7 @@
         HdmiDeviceInfo systemAudioDeviceInfo = getDeviceInfo(
                 localCecDevice.findAudioReceiverAddress());
 
-        // Condition 2: All AVB-capable audio outputs already use full/absolute volume behavior
+        // Condition 3: All AVB-capable audio outputs already use full/absolute volume behavior
         // We only need to check the first AVB-capable audio output because only TV panels
         // have more than one of them, and they always have the same volume behavior.
         @AudioManager.DeviceVolumeBehavior int currentVolumeBehavior =
@@ -4492,7 +4505,7 @@
         boolean alreadyUsingFullOrAbsoluteVolume =
                 FULL_AND_ABSOLUTE_VOLUME_BEHAVIORS.contains(currentVolumeBehavior);
 
-        // Condition 3: CEC volume is enabled
+        // Condition 4: CEC volume is enabled
         boolean cecVolumeEnabled =
                 getHdmiCecVolumeControl() == HdmiControlManager.VOLUME_CONTROL_ENABLED;
 
@@ -4508,7 +4521,7 @@
             return;
         }
 
-        // Condition 4: The System Audio device supports <Set Audio Volume Level>
+        // Condition 5: The System Audio device supports <Set Audio Volume Level>
         switch (systemAudioDeviceInfo.getDeviceFeatures().getSetAudioVolumeLevelSupport()) {
             case DeviceFeatures.FEATURE_SUPPORTED:
                 if (currentVolumeBehavior != AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) {
@@ -4555,6 +4568,8 @@
      * are currently used. Removes the action for handling volume updates for these behaviors.
      */
     private void switchToFullVolumeBehavior() {
+        Slog.d(TAG, "Switching to full volume behavior");
+
         if (playback() != null) {
             playback().removeAvbAudioStatusAction();
         } else if (tv() != null) {
@@ -4596,12 +4611,14 @@
         // Otherwise, enable adjust-only AVB on TVs only.
         if (systemAudioDevice.getDeviceFeatures().getSetAudioVolumeLevelSupport()
                 == DeviceFeatures.FEATURE_SUPPORTED) {
+            Slog.d(TAG, "Enabling absolute volume behavior");
             for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) {
                 getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
                         device, volumeInfo, mServiceThreadExecutor,
                         mAbsoluteVolumeChangedListener, true);
             }
         } else if (tv() != null) {
+            Slog.d(TAG, "Enabling adjust-only absolute volume behavior");
             for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) {
                 getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior(
                         device, volumeInfo, mServiceThreadExecutor,
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 4089a81..dda50ca 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -167,13 +167,17 @@
 
     /**
      * Indicates that the IME window has re-parented to the new target when the IME control changed.
+     *
+     * @param displayId the display hosting the IME window
      */
-    public abstract void onImeParentChanged();
+    public abstract void onImeParentChanged(int displayId);
 
     /**
-     * Destroys the IME surface.
+     * Destroys the IME surface for the given display.
+     *
+     * @param displayId the display hosting the IME window
      */
-    public abstract void removeImeSurface();
+    public abstract void removeImeSurface(int displayId);
 
     /**
      * Updates the IME visibility, back disposition and show IME picker status for SystemUI.
@@ -298,11 +302,11 @@
                 }
 
                 @Override
-                public void onImeParentChanged() {
+                public void onImeParentChanged(int displayId) {
                 }
 
                 @Override
-                public void removeImeSurface() {
+                public void removeImeSurface(int displayId) {
                 }
 
                 @Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 16e043c..0d29b7d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5671,7 +5671,7 @@
         }
 
         @Override
-        public void onImeParentChanged() {
+        public void onImeParentChanged(int displayId) {
             synchronized (ImfLock.class) {
                 // Hide the IME method menu only when the IME surface parent is changed by the
                 // input target changed, in case seeing the dialog dismiss flickering during
@@ -5683,7 +5683,7 @@
         }
 
         @Override
-        public void removeImeSurface() {
+        public void removeImeSurface(int displayId) {
             mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
         }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index 773293f..a88d85e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -229,31 +229,6 @@
             }
         }
 
-        private static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
-                String enabledInputMethodsStr,
-                TextUtils.SimpleStringSplitter inputMethodSplitter,
-                TextUtils.SimpleStringSplitter subtypeSplitter) {
-            ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
-            if (TextUtils.isEmpty(enabledInputMethodsStr)) {
-                return imsList;
-            }
-            inputMethodSplitter.setString(enabledInputMethodsStr);
-            while (inputMethodSplitter.hasNext()) {
-                String nextImsStr = inputMethodSplitter.next();
-                subtypeSplitter.setString(nextImsStr);
-                if (subtypeSplitter.hasNext()) {
-                    ArrayList<String> subtypeHashes = new ArrayList<>();
-                    // The first element is ime id.
-                    String imeId = subtypeSplitter.next();
-                    while (subtypeSplitter.hasNext()) {
-                        subtypeHashes.add(subtypeSplitter.next());
-                    }
-                    imsList.add(new Pair<>(imeId, subtypeHashes));
-                }
-            }
-            return imsList;
-        }
-
         InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId,
                 boolean copyOnWrite) {
             mMethodMap = methodMap;
@@ -351,18 +326,30 @@
         }
 
         List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
-            return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR),
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR));
-        }
-
-        List<String> getEnabledInputMethodNames() {
-            List<String> result = new ArrayList<>();
-            for (Pair<String, ArrayList<String>> pair :
-                    getEnabledInputMethodsAndSubtypeListLocked()) {
-                result.add(pair.first);
+            final String enabledInputMethodsStr = getEnabledInputMethodsStr();
+            final TextUtils.SimpleStringSplitter inputMethodSplitter =
+                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
+            final TextUtils.SimpleStringSplitter subtypeSplitter =
+                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
+            final ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
+            if (TextUtils.isEmpty(enabledInputMethodsStr)) {
+                return imsList;
             }
-            return result;
+            inputMethodSplitter.setString(enabledInputMethodsStr);
+            while (inputMethodSplitter.hasNext()) {
+                String nextImsStr = inputMethodSplitter.next();
+                subtypeSplitter.setString(nextImsStr);
+                if (subtypeSplitter.hasNext()) {
+                    ArrayList<String> subtypeHashes = new ArrayList<>();
+                    // The first element is ime id.
+                    String imeId = subtypeSplitter.next();
+                    while (subtypeSplitter.hasNext()) {
+                        subtypeHashes.add(subtypeSplitter.next());
+                    }
+                    imsList.add(new Pair<>(imeId, subtypeHashes));
+                }
+            }
+            return imsList;
         }
 
         void appendAndPutEnabledInputMethodLocked(String id) {
diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
index f2dcba4..5514ec7 100644
--- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
@@ -59,6 +59,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
@@ -67,7 +68,7 @@
 import com.android.server.integrity.model.IntegrityCheckResult;
 import com.android.server.integrity.model.RuleMetadata;
 import com.android.server.pm.PackageManagerServiceUtils;
-import com.android.server.pm.parsing.PackageParser2;
+import com.android.server.pm.parsing.PackageParserUtils;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -141,7 +142,7 @@
         return new AppIntegrityManagerServiceImpl(
                 context,
                 LocalServices.getService(PackageManagerInternal.class),
-                PackageParser2::forParsingFileWithDefaults,
+                PackageParserUtils::forParsingFileWithDefaults,
                 RuleEvaluationEngine.getRuleEvaluationEngine(),
                 IntegrityFileManager.getInstance(),
                 handlerThread.getThreadHandler());
diff --git a/services/core/java/com/android/server/location/altitude/AltitudeService.java b/services/core/java/com/android/server/location/altitude/AltitudeService.java
index b321e4d..97bf354 100644
--- a/services/core/java/com/android/server/location/altitude/AltitudeService.java
+++ b/services/core/java/com/android/server/location/altitude/AltitudeService.java
@@ -19,6 +19,8 @@
 import android.content.Context;
 import android.frameworks.location.altitude.AddMslAltitudeToLocationRequest;
 import android.frameworks.location.altitude.AddMslAltitudeToLocationResponse;
+import android.frameworks.location.altitude.GetGeoidHeightRequest;
+import android.frameworks.location.altitude.GetGeoidHeightResponse;
 import android.frameworks.location.altitude.IAltitudeService;
 import android.location.Location;
 import android.location.altitude.AltitudeConverter;
@@ -52,15 +54,47 @@
         location.setLongitude(request.longitudeDegrees);
         location.setAltitude(request.altitudeMeters);
         location.setVerticalAccuracyMeters(request.verticalAccuracyMeters);
+
+        AddMslAltitudeToLocationResponse response = new AddMslAltitudeToLocationResponse();
         try {
             mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
         } catch (IOException e) {
-            throw new RemoteException(e);
+            response.success = false;
+            return response;
         }
-
-        AddMslAltitudeToLocationResponse response = new AddMslAltitudeToLocationResponse();
         response.mslAltitudeMeters = location.getMslAltitudeMeters();
         response.mslAltitudeAccuracyMeters = location.getMslAltitudeAccuracyMeters();
+        response.success = true;
+        return response;
+    }
+
+    @Override
+    public GetGeoidHeightResponse getGeoidHeight(GetGeoidHeightRequest request)
+            throws RemoteException {
+        Location location = new Location("");
+        location.setLatitude(request.latitudeDegrees);
+        location.setLongitude(request.longitudeDegrees);
+        location.setAltitude(0.0);
+        location.setVerticalAccuracyMeters(0.0f);
+
+        GetGeoidHeightResponse response = new GetGeoidHeightResponse();
+        try {
+            mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
+        } catch (IOException e) {
+            response.success = false;
+            return response;
+        }
+        // The geoid height for a location with zero WGS84 altitude is equal in value to the
+        // negative of the corresponding MSL altitude.
+        response.geoidHeightMeters = -location.getMslAltitudeMeters();
+        // The geoid height error for a location with zero vertical accuracy is equal in value to
+        // the corresponding MSL altitude accuracy.
+        response.geoidHeightErrorMeters = location.getMslAltitudeAccuracyMeters();
+        // The expiration distance and additional error are currently set to constants used by
+        // health services.
+        response.expirationDistanceMeters = 10000.0;
+        response.additionalGeoidHeightErrorMeters = 0.707f;
+        response.success = true;
         return response;
     }
 
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index 27b01a5..9c4225d 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -67,6 +67,7 @@
 import android.location.LocationManager;
 import android.location.LocationRequest;
 import android.location.LocationResult;
+import android.location.LocationResult.BadLocationException;
 import android.location.flags.Flags;
 import android.location.provider.ProviderProperties;
 import android.location.provider.ProviderRequest;
@@ -1380,7 +1381,11 @@
 
         location.setExtras(mLocationExtras.getBundle());
 
-        reportLocation(LocationResult.wrap(location).validate());
+        try {
+            reportLocation(LocationResult.wrap(location).validate());
+        } catch (BadLocationException e) {
+            throw new IllegalArgumentException(e);
+        }
 
         if (mStarted) {
             mGnssMetrics.logReceivedLocationStatus(hasLatLong);
@@ -1751,7 +1756,11 @@
                 }
             }
 
-            reportLocation(LocationResult.wrap(locations).validate());
+            try {
+                reportLocation(LocationResult.wrap(locations).validate());
+            } catch (BadLocationException e) {
+                throw new IllegalArgumentException(e);
+            }
         }
 
         Runnable[] listeners;
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 78c8cde..403b421 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 
 import android.content.Context;
+import android.location.flags.Flags;
 import android.net.ConnectivityManager;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -48,6 +49,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Handles network connection requests and network state change updates for AGPS data download.
@@ -91,6 +93,10 @@
     // network with SUPL connectivity or report an error.
     private static final int SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS = 20 * 1000;
 
+    // If the chipset does not request to release a SUPL connection before the specified timeout in
+    // milliseconds, the connection will be automatically released.
+    private static final long SUPL_CONNECTION_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
     private static final int HASH_MAP_INITIAL_CAPACITY_TO_TRACK_CONNECTED_NETWORKS = 5;
 
     // Keeps track of networks and their state as notified by the network request callbacks.
@@ -121,6 +127,8 @@
     private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
     private final PowerManager.WakeLock mWakeLock;
 
+    private final Object mSuplConnectionReleaseOnTimeoutToken = new Object();
+
     /**
      * Network attributes needed when updating HAL about network connectivity status changes.
      */
@@ -609,6 +617,13 @@
                     mSuplConnectivityCallback,
                     mHandler,
                     SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS);
+            if (Flags.releaseSuplConnectionOnTimeout()) {
+                // Schedule to release the SUPL connection after timeout
+                mHandler.removeCallbacksAndMessages(mSuplConnectionReleaseOnTimeoutToken);
+                mHandler.postDelayed(() -> handleReleaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN),
+                        mSuplConnectionReleaseOnTimeoutToken,
+                        SUPL_CONNECTION_TIMEOUT_MILLIS);
+            }
         } catch (RuntimeException e) {
             Log.e(TAG, "Failed to request network.", e);
             mSuplConnectivityCallback = null;
@@ -639,6 +654,10 @@
             Log.d(TAG, message);
         }
 
+        if (Flags.releaseSuplConnectionOnTimeout()) {
+            // Remove pending task to avoid releasing an incorrect connection
+            mHandler.removeCallbacksAndMessages(mSuplConnectionReleaseOnTimeoutToken);
+        }
         if (mAGpsDataConnectionState == AGPS_DATA_CONNECTION_CLOSED) {
             return;
         }
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index d3eb5a9..7d44aec 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -64,6 +64,7 @@
 import android.location.LocationManagerInternal.ProviderEnabledListener;
 import android.location.LocationRequest;
 import android.location.LocationResult;
+import android.location.LocationResult.BadLocationException;
 import android.location.altitude.AltitudeConverter;
 import android.location.provider.IProviderRequestListener;
 import android.location.provider.ProviderProperties;
@@ -910,7 +911,8 @@
                                         < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) {
                                     if (D) {
                                         Log.v(TAG, mName + " provider registration " + getIdentity()
-                                                + " dropped delivery - too fast");
+                                                + " dropped delivery - too fast (deltaMs="
+                                                + deltaMs + ").");
                                     }
                                     return false;
                                 }
@@ -1734,12 +1736,6 @@
             return null;
         }
 
-        // lastly - note app ops
-        if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(permissionLevel),
-                identity)) {
-            return null;
-        }
-
         Location location = getPermittedLocation(
                 getLastLocationUnsafe(
                         identity.getUserId(),
@@ -1748,10 +1744,18 @@
                         Long.MAX_VALUE),
                 permissionLevel);
 
-        if (location != null && identity.getPid() == Process.myPid()) {
+        if (location != null) {
+            // lastly - note app ops
+            if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(permissionLevel),
+                    identity)) {
+                return null;
+            }
+
             // if delivering to the same process, make a copy of the location first (since
             // location is mutable)
-            location = new Location(location);
+            if (identity.getPid() == Process.myPid()) {
+                location = new Location(location);
+            }
         }
 
         return location;
@@ -2572,29 +2576,17 @@
     @GuardedBy("mMultiplexerLock")
     @Nullable
     private LocationResult processReportedLocation(LocationResult locationResult) {
-        LocationResult processed = locationResult.filter(location -> {
-            if (!location.isMock()) {
-                if (location.getLatitude() == 0 && location.getLongitude() == 0) {
-                    Log.e(TAG, "blocking 0,0 location from " + mName + " provider");
-                    return false;
-                }
-            }
-
-            if (!location.isComplete()) {
-                Log.e(TAG, "blocking incomplete location from " + mName + " provider");
-                return false;
-            }
-
-            return true;
-        });
-        if (processed == null) {
+        try {
+            locationResult.validate();
+        } catch (BadLocationException e) {
+            Log.e(TAG, "Dropping invalid locations: " + e);
             return null;
         }
 
         // Attempt to add a missing MSL altitude on behalf of the provider.
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_LOCATION,
                 "enable_location_provider_manager_msl", true)) {
-            return processed.map(location -> {
+            return locationResult.map(location -> {
                 if (!location.hasMslAltitude() && location.hasAltitude()) {
                     try {
                         Location locationCopy = new Location(location);
@@ -2624,7 +2616,7 @@
                 return location;
             });
         }
-        return processed;
+        return locationResult;
     }
 
     @GuardedBy("mMultiplexerLock")
diff --git a/services/core/java/com/android/server/location/provider/MockLocationProvider.java b/services/core/java/com/android/server/location/provider/MockLocationProvider.java
index 52b04d4..4efacd7 100644
--- a/services/core/java/com/android/server/location/provider/MockLocationProvider.java
+++ b/services/core/java/com/android/server/location/provider/MockLocationProvider.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.location.Location;
 import android.location.LocationResult;
+import android.location.LocationResult.BadLocationException;
 import android.location.provider.ProviderProperties;
 import android.location.provider.ProviderRequest;
 import android.location.util.identity.CallerIdentity;
@@ -55,7 +56,11 @@
         Location location = new Location(l);
         location.setIsFromMockProvider(true);
         mLocation = location;
-        reportLocation(LocationResult.wrap(location).validate());
+        try {
+            reportLocation(LocationResult.wrap(location).validate());
+        } catch (BadLocationException e) {
+            throw new IllegalArgumentException(e);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java b/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java
index 05966da..a597edd 100644
--- a/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java
+++ b/services/core/java/com/android/server/location/provider/proxy/ProxyLocationProvider.java
@@ -305,7 +305,7 @@
                     return;
                 }
 
-                reportLocation(LocationResult.wrap(location).validate());
+                reportLocation(LocationResult.wrap(location));
             }
         }
 
@@ -316,8 +316,7 @@
                 if (mProxy != this) {
                     return;
                 }
-
-                reportLocation(LocationResult.wrap(locations).validate());
+                reportLocation(LocationResult.wrap(locations));
             }
         }
 
diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
index 8504495..0eb9166 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
@@ -87,6 +87,8 @@
 
     @NonNull private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallbackImpl();
 
+    @MediaRoute2Info.SuitabilityStatus private final int mBuiltInSpeakerSuitabilityStatus;
+
     @NonNull
     private final AudioManager.OnDevicesForAttributesChangedListener
             mOnDevicesForAttributesChangedListener = this::onDevicesForAttributesChangedListener;
@@ -113,6 +115,10 @@
         mHandler = new Handler(Objects.requireNonNull(looper));
         mStrategyForMedia = Objects.requireNonNull(strategyForMedia);
         mOnDeviceRouteChangedListener = Objects.requireNonNull(onDeviceRouteChangedListener);
+
+        mBuiltInSpeakerSuitabilityStatus =
+                DeviceRouteController.getBuiltInSpeakerSuitabilityStatus(mContext);
+
         mBluetoothRouteController =
                 new AudioPoliciesBluetoothRouteController(
                         mContext, btAdapter, this::rebuildAvailableRoutesAndNotify);
@@ -373,14 +379,19 @@
             // from getting an id using BluetoothRouteController#getRouteIdForBluetoothAddress.
             routeId = systemRouteInfo.mDefaultRouteId;
         }
-        return new MediaRoute2Info.Builder(routeId, humanReadableName)
+        MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(routeId, humanReadableName)
                 .setType(systemRouteInfo.mMediaRoute2InfoType)
                 .setAddress(address)
                 .setSystemRoute(true)
                 .addFeature(FEATURE_LIVE_AUDIO)
                 .addFeature(FEATURE_LOCAL_PLAYBACK)
-                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
-                .build();
+                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED);
+
+        if (systemRouteInfo.mMediaRoute2InfoType == MediaRoute2Info.TYPE_BUILTIN_SPEAKER) {
+            builder.setSuitabilityStatus(mBuiltInSpeakerSuitabilityStatus);
+        }
+
+        return builder.build();
     }
 
     /**
diff --git a/services/core/java/com/android/server/media/DeviceRouteController.java b/services/core/java/com/android/server/media/DeviceRouteController.java
index 9f175a9..8b62cc9 100644
--- a/services/core/java/com/android/server/media/DeviceRouteController.java
+++ b/services/core/java/com/android/server/media/DeviceRouteController.java
@@ -81,6 +81,30 @@
         }
     }
 
+    /** Returns device route availability status. */
+    @MediaRoute2Info.SuitabilityStatus
+    static int getBuiltInSpeakerSuitabilityStatus(@NonNull Context context) {
+        if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+            // Route is always suitable if the flag is disabled.
+            return MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER;
+        }
+
+        int availabilityStatus =
+                context.getResources()
+                        .getInteger(
+                                com.android.internal.R.integer
+                                        .config_mediaRouter_builtInSpeakerSuitability);
+
+        switch (availabilityStatus) {
+            case MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER:
+            case MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER:
+            case MediaRoute2Info.SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER:
+                return availabilityStatus;
+            default:
+                return MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER;
+        }
+    }
+
     /** Returns the currently selected device (built-in or wired) route. */
     @NonNull
     MediaRoute2Info getSelectedRoute();
diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
index c0f2834..65b0ad0 100644
--- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
@@ -72,6 +72,8 @@
     @NonNull
     private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();
 
+    @MediaRoute2Info.SuitabilityStatus private final int mBuiltInSpeakerSuitabilityStatus;
+
     private int mDeviceVolume;
     private MediaRoute2Info mDeviceRoute;
 
@@ -90,6 +92,9 @@
         mAudioManager = audioManager;
         mAudioService = audioService;
 
+        mBuiltInSpeakerSuitabilityStatus =
+                DeviceRouteController.getBuiltInSpeakerSuitabilityStatus(mContext);
+
         AudioRoutesInfo newAudioRoutes = null;
         try {
             newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
@@ -165,19 +170,28 @@
         }
 
         synchronized (this) {
-            return new MediaRoute2Info.Builder(
-                    DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString())
-                    .setVolumeHandling(mAudioManager.isVolumeFixed()
-                            ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
-                            : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
-                    .setVolume(mDeviceVolume)
-                    .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
-                    .setType(type)
-                    .addFeature(FEATURE_LIVE_AUDIO)
-                    .addFeature(FEATURE_LIVE_VIDEO)
-                    .addFeature(FEATURE_LOCAL_PLAYBACK)
-                    .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
-                    .build();
+            MediaRoute2Info.Builder builder =
+                    new MediaRoute2Info.Builder(
+                                    DEVICE_ROUTE_ID,
+                                    mContext.getResources().getText(name).toString())
+                            .setVolumeHandling(
+                                    mAudioManager.isVolumeFixed()
+                                            ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
+                                            : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
+                            .setVolume(mDeviceVolume)
+                            .setVolumeMax(
+                                    mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+                            .setType(type)
+                            .addFeature(FEATURE_LIVE_AUDIO)
+                            .addFeature(FEATURE_LIVE_VIDEO)
+                            .addFeature(FEATURE_LOCAL_PLAYBACK)
+                            .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED);
+
+            if (type == TYPE_BUILTIN_SPEAKER) {
+                builder.setSuitabilityStatus(mBuiltInSpeakerSuitabilityStatus);
+            }
+
+            return builder.build();
         }
     }
 
diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java
index 8149847..1bc2a5e 100644
--- a/services/core/java/com/android/server/media/MediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java
@@ -24,6 +24,7 @@
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
 import android.os.Bundle;
+import android.os.UserHandle;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -54,8 +55,15 @@
         mCallback = callback;
     }
 
-    public abstract void requestCreateSession(long requestId, String packageName, String routeId,
-            @Nullable Bundle sessionHints);
+    public abstract void requestCreateSession(
+            long requestId,
+            String packageName,
+            String routeId,
+            @Nullable Bundle sessionHints,
+            @RoutingSessionInfo.TransferReason int transferReason,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName);
+
     public abstract void releaseSession(long requestId, String sessionId);
 
     public abstract void updateDiscoveryPreference(
@@ -63,7 +71,14 @@
 
     public abstract void selectRoute(long requestId, String sessionId, String routeId);
     public abstract void deselectRoute(long requestId, String sessionId, String routeId);
-    public abstract void transferToRoute(long requestId, String sessionId, String routeId);
+
+    public abstract void transferToRoute(
+            long requestId,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName,
+            String sessionId,
+            String routeId,
+            @RoutingSessionInfo.TransferReason int transferReason);
 
     public abstract void setRouteVolume(long requestId, String routeId, int volume);
     public abstract void setSessionVolume(long requestId, String sessionId, int volume);
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index 330818e..ae889d8 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -98,8 +98,14 @@
     }
 
     @Override
-    public void requestCreateSession(long requestId, String packageName, String routeId,
-            Bundle sessionHints) {
+    public void requestCreateSession(
+            long requestId,
+            String packageName,
+            String routeId,
+            Bundle sessionHints,
+            @RoutingSessionInfo.TransferReason int transferReason,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         if (mConnectionReady) {
             mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints);
             updateBinding();
@@ -141,7 +147,13 @@
     }
 
     @Override
-    public void transferToRoute(long requestId, String sessionId, String routeId) {
+    public void transferToRoute(
+            long requestId,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName,
+            String sessionId,
+            String routeId,
+            @RoutingSessionInfo.TransferReason int transferReason) {
         if (mConnectionReady) {
             mActiveConnection.transferToRoute(requestId, sessionId, routeId);
         }
@@ -649,6 +661,14 @@
                                     + "Disallowed route: "
                                     + route);
                 }
+
+                if (route.getSuitabilityStatus()
+                        == MediaRoute2Info.SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER) {
+                    throw new SecurityException(
+                            "Only the system is allowed to set not suitable for transfer status. "
+                                    + "Disallowed route: "
+                                    + route);
+                }
             }
 
             Connection connection = mConnectionRef.get();
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 5e18727..38f0df4 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -337,18 +337,47 @@
         }
     }
 
-    public void requestCreateSessionWithRouter2(@NonNull IMediaRouter2 router, int requestId,
-            long managerRequestId, @NonNull RoutingSessionInfo oldSession,
-            @NonNull MediaRoute2Info route, Bundle sessionHints) {
+    public void requestCreateSessionWithRouter2(
+            @NonNull IMediaRouter2 router,
+            int requestId,
+            long managerRequestId,
+            @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route,
+            Bundle sessionHints,
+            @Nullable UserHandle transferInitiatorUserHandle,
+            @Nullable String transferInitiatorPackageName) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(oldSession, "oldSession must not be null");
         Objects.requireNonNull(route, "route must not be null");
 
+        synchronized (mLock) {
+            if (managerRequestId == MediaRoute2ProviderService.REQUEST_ID_NONE
+                    || transferInitiatorUserHandle == null
+                    || transferInitiatorPackageName == null) {
+                final IBinder binder = router.asBinder();
+                final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+                transferInitiatorUserHandle = Binder.getCallingUserHandle();
+                if (routerRecord != null) {
+                    transferInitiatorPackageName = routerRecord.mPackageName;
+                } else {
+                    transferInitiatorPackageName = mContext.getPackageName();
+                }
+            }
+        }
+
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                requestCreateSessionWithRouter2Locked(requestId, managerRequestId,
-                        router, oldSession, route, sessionHints);
+                requestCreateSessionWithRouter2Locked(
+                        requestId,
+                        managerRequestId,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName,
+                        router,
+                        oldSession,
+                        route,
+                        sessionHints);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -399,10 +428,11 @@
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
         }
 
+        UserHandle userHandle = Binder.getCallingUserHandle();
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                transferToRouteWithRouter2Locked(router, uniqueSessionId, route);
+                transferToRouteWithRouter2Locked(router, userHandle, uniqueSessionId, route);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -588,16 +618,28 @@
         }
     }
 
-    public void requestCreateSessionWithManager(@NonNull IMediaRouter2Manager manager,
-            int requestId, @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
+    public void requestCreateSessionWithManager(
+            @NonNull IMediaRouter2Manager manager,
+            int requestId,
+            @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         Objects.requireNonNull(manager, "manager must not be null");
         Objects.requireNonNull(oldSession, "oldSession must not be null");
         Objects.requireNonNull(route, "route must not be null");
+        Objects.requireNonNull(transferInitiatorUserHandle);
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route);
+                requestCreateSessionWithManagerLocked(
+                        requestId,
+                        manager,
+                        oldSession,
+                        route,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -640,18 +682,32 @@
         }
     }
 
-    public void transferToRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
-            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+    public void transferToRouteWithManager(
+            @NonNull IMediaRouter2Manager manager,
+            int requestId,
+            @NonNull String uniqueSessionId,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
         }
         Objects.requireNonNull(route, "route must not be null");
+        Objects.requireNonNull(transferInitiatorUserHandle);
+        Objects.requireNonNull(transferInitiatorPackageName);
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                transferToRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
+                transferToRouteWithManagerLocked(
+                        requestId,
+                        manager,
+                        uniqueSessionId,
+                        route,
+                        RoutingSessionInfo.TRANSFER_REASON_SYSTEM_REQUEST,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -1038,9 +1094,15 @@
     }
 
     @GuardedBy("mLock")
-    private void requestCreateSessionWithRouter2Locked(int requestId, long managerRequestId,
-            @NonNull IMediaRouter2 router, @NonNull RoutingSessionInfo oldSession,
-            @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
+    private void requestCreateSessionWithRouter2Locked(
+            int requestId,
+            long managerRequestId,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName,
+            @NonNull IMediaRouter2 router,
+            @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route,
+            @Nullable Bundle sessionHints) {
         final IBinder binder = router.asBinder();
         final RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
@@ -1114,9 +1176,16 @@
 
         long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
         routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler,
+                obtainMessage(
+                        UserHandler::requestCreateSessionWithRouter2OnHandler,
                         routerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, managerRequestId, routerRecord, oldSession, route,
+                        uniqueRequestId,
+                        managerRequestId,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName,
+                        routerRecord,
+                        oldSession,
+                        route,
                         sessionHints));
     }
 
@@ -1165,8 +1234,11 @@
     }
 
     @GuardedBy("mLock")
-    private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
-            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+    private void transferToRouteWithRouter2Locked(
+            @NonNull IMediaRouter2 router,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String uniqueSessionId,
+            @NonNull MediaRoute2Info route) {
         final IBinder binder = router.asBinder();
         final RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
@@ -1191,9 +1263,16 @@
                             routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID)));
         } else {
             routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::transferToRouteOnHandler,
+                    obtainMessage(
+                            UserHandler::transferToRouteOnHandler,
                             routerRecord.mUserRecord.mHandler,
-                            DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route));
+                            DUMMY_REQUEST_ID,
+                            transferInitiatorUserHandle,
+                            routerRecord.mPackageName,
+                            routerRecord,
+                            uniqueSessionId,
+                            route,
+                            RoutingSessionInfo.TRANSFER_REASON_APP));
         }
     }
 
@@ -1416,9 +1495,13 @@
     }
 
     @GuardedBy("mLock")
-    private void requestCreateSessionWithManagerLocked(int requestId,
-            @NonNull IMediaRouter2Manager manager, @NonNull RoutingSessionInfo oldSession,
-            @NonNull MediaRoute2Info route) {
+    private void requestCreateSessionWithManagerLocked(
+            int requestId,
+            @NonNull IMediaRouter2Manager manager,
+            @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
         if (managerRecord == null) {
             return;
@@ -1464,9 +1547,16 @@
         // Before requesting to the provider, get session hints from the media router.
         // As a return, media router will request to create a session.
         routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::requestRouterCreateSessionOnHandler,
+                obtainMessage(
+                        UserHandler::requestRouterCreateSessionOnHandler,
                         routerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, routerRecord, managerRecord, oldSession, route));
+                        uniqueRequestId,
+                        routerRecord,
+                        managerRecord,
+                        oldSession,
+                        route,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName));
     }
 
     @GuardedBy("mLock")
@@ -1521,9 +1611,14 @@
     }
 
     @GuardedBy("mLock")
-    private void transferToRouteWithManagerLocked(int requestId,
+    private void transferToRouteWithManagerLocked(
+            int requestId,
             @NonNull IMediaRouter2Manager manager,
-            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+            @NonNull String uniqueSessionId,
+            @NonNull MediaRoute2Info route,
+            @RoutingSessionInfo.TransferReason int transferReason,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         final IBinder binder = manager.asBinder();
         ManagerRecord managerRecord = mAllManagerRecords.get(binder);
 
@@ -1541,9 +1636,16 @@
 
         long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
         managerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::transferToRouteOnHandler,
+                obtainMessage(
+                        UserHandler::transferToRouteOnHandler,
                         managerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, routerRecord, uniqueSessionId, route));
+                        uniqueRequestId,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName,
+                        routerRecord,
+                        uniqueSessionId,
+                        route,
+                        transferReason));
     }
 
     @GuardedBy("mLock")
@@ -1850,6 +1952,19 @@
             }
         }
 
+        public void notifySessionCreated(int requestId, @NonNull RoutingSessionInfo sessionInfo) {
+            try {
+                mRouter.notifySessionCreated(
+                        requestId, maybeClearTransferInitiatorIdentity(sessionInfo));
+            } catch (RemoteException ex) {
+                Slog.w(
+                        TAG,
+                        "Failed to notify router of the session creation."
+                                + " Router probably died.",
+                        ex);
+            }
+        }
+
         /**
          * Sends the corresponding router an update for the given session.
          *
@@ -1857,12 +1972,27 @@
          */
         public void notifySessionInfoChanged(RoutingSessionInfo sessionInfo) {
             try {
-                mRouter.notifySessionInfoChanged(sessionInfo);
+                mRouter.notifySessionInfoChanged(maybeClearTransferInitiatorIdentity(sessionInfo));
             } catch (RemoteException ex) {
                 Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex);
             }
         }
 
+        private RoutingSessionInfo maybeClearTransferInitiatorIdentity(
+                @NonNull RoutingSessionInfo sessionInfo) {
+            UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+            String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+
+            if (!Objects.equals(UserHandle.of(mUserRecord.mUserId), transferInitiatorUserHandle)
+                    || !Objects.equals(mPackageName, transferInitiatorPackageName)) {
+                return new RoutingSessionInfo.Builder(sessionInfo)
+                        .setTransferInitiator(null, null)
+                        .build();
+            }
+
+            return sessionInfo;
+        }
+
         /**
          * Returns a filtered copy of {@code routes} that contains only the routes that are {@link
          * MediaRoute2Info#isVisibleTo visible} to the router corresponding to this record.
@@ -2307,9 +2437,14 @@
             return -1;
         }
 
-        private void requestRouterCreateSessionOnHandler(long uniqueRequestId,
-                @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord,
-                @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
+        private void requestRouterCreateSessionOnHandler(
+                long uniqueRequestId,
+                @NonNull RouterRecord routerRecord,
+                @NonNull ManagerRecord managerRecord,
+                @NonNull RoutingSessionInfo oldSession,
+                @NonNull MediaRoute2Info route,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName) {
             try {
                 if (route.isSystemRoute() && !routerRecord.hasSystemRoutingPermission()) {
                     // The router lacks permission to modify system routing, so we hide system
@@ -2317,7 +2452,11 @@
                     route = mSystemProvider.getDefaultRoute();
                 }
                 routerRecord.mRouter.requestCreateSessionByManager(
-                        uniqueRequestId, oldSession, route);
+                        uniqueRequestId,
+                        oldSession,
+                        route,
+                        transferInitiatorUserHandle,
+                        transferInitiatorPackageName);
             } catch (RemoteException ex) {
                 Slog.w(TAG, "getSessionHintsForCreatingSessionOnHandler: "
                         + "Failed to request. Router probably died.", ex);
@@ -2326,10 +2465,15 @@
             }
         }
 
-        private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId,
-                long managerRequestId, @NonNull RouterRecord routerRecord,
+        private void requestCreateSessionWithRouter2OnHandler(
+                long uniqueRequestId,
+                long managerRequestId,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName,
+                @NonNull RouterRecord routerRecord,
                 @NonNull RoutingSessionInfo oldSession,
-                @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
+                @NonNull MediaRoute2Info route,
+                @Nullable Bundle sessionHints) {
 
             final MediaRoute2Provider provider = findProvider(route.getProviderId());
             if (provider == null) {
@@ -2345,8 +2489,19 @@
                             managerRequestId, oldSession, route);
             mSessionCreationRequests.add(request);
 
-            provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName,
-                    route.getOriginalId(), sessionHints);
+            int transferReason = RoutingSessionInfo.TRANSFER_REASON_APP;
+            if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) {
+                transferReason = RoutingSessionInfo.TRANSFER_REASON_SYSTEM_REQUEST;
+            }
+
+            provider.requestCreateSession(
+                    uniqueRequestId,
+                    routerRecord.mPackageName,
+                    route.getOriginalId(),
+                    sessionHints,
+                    transferReason,
+                    transferInitiatorUserHandle,
+                    transferInitiatorPackageName);
         }
 
         // routerRecord can be null if the session is system's or RCN.
@@ -2386,9 +2541,14 @@
         }
 
         // routerRecord can be null if the session is system's or RCN.
-        private void transferToRouteOnHandler(long uniqueRequestId,
+        private void transferToRouteOnHandler(
+                long uniqueRequestId,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName,
                 @Nullable RouterRecord routerRecord,
-                @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+                @NonNull String uniqueSessionId,
+                @NonNull MediaRoute2Info route,
+                @RoutingSessionInfo.TransferReason int transferReason) {
             if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route,
                     "transferring to")) {
                 return;
@@ -2399,8 +2559,13 @@
             if (provider == null) {
                 return;
             }
-            provider.transferToRoute(uniqueRequestId, getOriginalId(uniqueSessionId),
-                    route.getOriginalId());
+            provider.transferToRoute(
+                    uniqueRequestId,
+                    transferInitiatorUserHandle,
+                    transferInitiatorPackageName,
+                    getOriginalId(uniqueSessionId),
+                    route.getOriginalId(),
+                    transferReason);
         }
 
         // routerRecord is null if and only if the session is created without the request, which
@@ -2535,6 +2700,7 @@
                 // session info from them.
                 sessionInfo = mSystemProvider.getDefaultSessionInfo();
             }
+            // TODO: b/279555229 - replace with matchingRequest.mRouterRecord.notifySessionCreated.
             notifySessionCreatedToRouter(
                     matchingRequest.mRouterRecord,
                     toOriginalRequestId(uniqueRequestId),
@@ -2648,12 +2814,7 @@
 
         private void notifySessionCreatedToRouter(@NonNull RouterRecord routerRecord,
                 int requestId, @NonNull RoutingSessionInfo sessionInfo) {
-            try {
-                routerRecord.mRouter.notifySessionCreated(requestId, sessionInfo);
-            } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to notify router of the session creation."
-                        + " Router probably died.", ex);
-            }
+            routerRecord.notifySessionCreated(requestId, sessionInfo);
         }
 
         private void notifySessionCreationFailedToRouter(@NonNull RouterRecord routerRecord,
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index e562b3f..7dd1314 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -461,11 +461,24 @@
 
     // Binder call
     @Override
-    public void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId,
-            long managerRequestId, RoutingSessionInfo oldSession,
-            MediaRoute2Info route, Bundle sessionHints) {
-        mService2.requestCreateSessionWithRouter2(router, requestId, managerRequestId,
-                oldSession, route, sessionHints);
+    public void requestCreateSessionWithRouter2(
+            IMediaRouter2 router,
+            int requestId,
+            long managerRequestId,
+            RoutingSessionInfo oldSession,
+            MediaRoute2Info route,
+            Bundle sessionHints,
+            @Nullable UserHandle transferInitiatorUserHandle,
+            @Nullable String transferInitiatorPackageName) {
+        mService2.requestCreateSessionWithRouter2(
+                router,
+                requestId,
+                managerRequestId,
+                oldSession,
+                route,
+                sessionHints,
+                transferInitiatorUserHandle,
+                transferInitiatorPackageName);
     }
 
     // Binder call
@@ -580,9 +593,20 @@
 
     // Binder call
     @Override
-    public void requestCreateSessionWithManager(IMediaRouter2Manager manager,
-            int requestId, RoutingSessionInfo oldSession, MediaRoute2Info route) {
-        mService2.requestCreateSessionWithManager(manager, requestId, oldSession, route);
+    public void requestCreateSessionWithManager(
+            IMediaRouter2Manager manager,
+            int requestId,
+            RoutingSessionInfo oldSession,
+            MediaRoute2Info route,
+            UserHandle transferInitiatorUserHandle,
+            String transferInitiatorPackageName) {
+        mService2.requestCreateSessionWithManager(
+                manager,
+                requestId,
+                oldSession,
+                route,
+                transferInitiatorUserHandle,
+                transferInitiatorPackageName);
     }
 
     // Binder call
@@ -601,9 +625,20 @@
 
     // Binder call
     @Override
-    public void transferToRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String sessionId, MediaRoute2Info route) {
-        mService2.transferToRouteWithManager(manager, requestId, sessionId, route);
+    public void transferToRouteWithManager(
+            IMediaRouter2Manager manager,
+            int requestId,
+            String sessionId,
+            MediaRoute2Info route,
+            UserHandle transferInitiatorUserHandle,
+            String transferInitiatorPackageName) {
+        mService2.transferToRouteWithManager(
+                manager,
+                requestId,
+                sessionId,
+                route,
+                transferInitiatorUserHandle,
+                transferInitiatorPackageName);
     }
 
     // Binder call
diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java
index b424c20..07b333a 100644
--- a/services/core/java/com/android/server/media/MediaSession2Record.java
+++ b/services/core/java/com/android/server/media/MediaSession2Record.java
@@ -16,6 +16,7 @@
 
 package com.android.server.media;
 
+import android.app.ForegroundServiceDelegationOptions;
 import android.media.MediaController2;
 import android.media.Session2CommandGroup;
 import android.media.Session2Token;
@@ -89,6 +90,12 @@
     }
 
     @Override
+    public ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions() {
+        // TODO: Implement when MediaSession2 knows about its owner pid.
+        return null;
+    }
+
+    @Override
     public boolean isSystemPriority() {
         // System priority session is currently only allowed for telephony, so it's OK to stick to
         // the media1 API at this moment.
@@ -217,7 +224,8 @@
             synchronized (mLock) {
                 service = mService;
             }
-            service.onSessionPlaybackStateChanged(MediaSession2Record.this, playbackActive);
+            service.onSessionPlaybackStateChanged(
+                    MediaSession2Record.this, playbackActive, /* playbackState= */ null);
         }
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 994d3ca..cce66e2 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -29,6 +29,7 @@
 import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
 import android.app.PendingIntent;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
@@ -182,6 +183,8 @@
     private final Context mContext;
     private final boolean mVolumeAdjustmentForRemoteGroupSessions;
 
+    private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
+
     private final Object mLock = new Object();
     private final CopyOnWriteArrayList<ISessionControllerCallbackHolder>
             mControllerCallbackHolders = new CopyOnWriteArrayList<>();
@@ -244,10 +247,32 @@
         mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
 
+        mForegroundServiceDelegationOptions = createForegroundServiceDelegationOptions();
+
         // May throw RemoteException if the session app is killed.
         mSessionCb.mCb.asBinder().linkToDeath(this, 0);
     }
 
+    private ForegroundServiceDelegationOptions createForegroundServiceDelegationOptions() {
+        return new ForegroundServiceDelegationOptions.Builder()
+                .setClientPid(mOwnerPid)
+                .setClientUid(getUid())
+                .setClientPackageName(getPackageName())
+                .setClientAppThread(null)
+                .setSticky(false)
+                .setClientInstanceName(
+                        "MediaSessionFgsDelegate_"
+                                + getUid()
+                                + "_"
+                                + mOwnerPid
+                                + "_"
+                                + getPackageName())
+                .setForegroundServiceTypes(0)
+                .setDelegationService(
+                        ForegroundServiceDelegationOptions.DELEGATION_SERVICE_MEDIA_PLAYBACK)
+                .build();
+    }
+
     /**
      * Get the session binder for the {@link MediaSession}.
      *
@@ -681,6 +706,11 @@
         return mPackageName + "/" + mTag + " (userId=" + mUserId + ")";
     }
 
+    @Override
+    public ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions() {
+        return mForegroundServiceDelegationOptions;
+    }
+
     private void postAdjustLocalVolume(final int stream, final int direction, final int flags,
             final String callingOpPackageName, final int callingPid, final int callingUid,
             final boolean asSystemService, final boolean useSuggested,
@@ -1273,7 +1303,7 @@
             final long token = Binder.clearCallingIdentity();
             try {
                 mService.onSessionPlaybackStateChanged(
-                        MediaSessionRecord.this, shouldUpdatePriority);
+                        MediaSessionRecord.this, shouldUpdatePriority, mPlaybackState);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java
index 8f01f02..99c8ea9 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java
@@ -16,7 +16,9 @@
 
 package com.android.server.media;
 
+import android.app.ForegroundServiceDelegationOptions;
 import android.media.AudioManager;
+import android.media.session.PlaybackState;
 import android.os.ResultReceiver;
 import android.view.KeyEvent;
 
@@ -51,6 +53,15 @@
     int getUserId();
 
     /**
+     * Get the {@link ForegroundServiceDelegationOptions} needed for notifying activity manager
+     * service with changes in the {@link PlaybackState} for this session.
+     *
+     * @return the {@link ForegroundServiceDelegationOptions} needed for notifying the activity
+     *     manager service with changes in the {@link PlaybackState} for this session.
+     */
+    ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions();
+
+    /**
      * Check if this session has system priority and should receive media buttons before any other
      * sessions.
      *
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 2c59511..d4666ba 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -29,6 +29,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -59,6 +61,7 @@
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -144,6 +147,7 @@
     private AudioManager mAudioManager;
     private boolean mHasFeatureLeanback;
     private ActivityManagerLocal mActivityManagerLocal;
+    private ActivityManagerInternal mActivityManagerInternal;
 
     // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
     // It's always not null after the MediaSessionService is started.
@@ -229,6 +233,7 @@
         mContext.registerReceiver(mNotificationListenerEnabledChangedReceiver, filter);
 
         mActivityManagerLocal = LocalManagerRegistry.getManager(ActivityManagerLocal.class);
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
     }
 
     @Override
@@ -285,11 +290,31 @@
                 }
                 user.mPriorityStack.onSessionActiveStateChanged(record);
             }
-
+            notifyActivityManagerWithActiveStateChanges(record, record.isActive());
             mHandler.postSessionsChanged(record);
         }
     }
 
+    private void notifyActivityManagerWithActiveStateChanges(
+            MediaSessionRecordImpl record, boolean isActive) {
+        if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+            return;
+        }
+        ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
+                record.getForegroundServiceDelegationOptions();
+        if (foregroundServiceDelegationOptions == null) {
+            // This record doesn't support FGS delegation. In practice, this is MediaSession2.
+            return;
+        }
+        if (isActive) {
+            mActivityManagerInternal.startForegroundServiceDelegate(
+                    foregroundServiceDelegationOptions, /* connection= */ null);
+        } else {
+            mActivityManagerInternal.stopForegroundServiceDelegate(
+                    foregroundServiceDelegationOptions);
+        }
+    }
+
     // Currently only media1 can become global priority session.
     void setGlobalPrioritySession(MediaSessionRecord record) {
         synchronized (mLock) {
@@ -371,8 +396,10 @@
         }
     }
 
-    void onSessionPlaybackStateChanged(MediaSessionRecordImpl record,
-            boolean shouldUpdatePriority) {
+    void onSessionPlaybackStateChanged(
+            MediaSessionRecordImpl record,
+            boolean shouldUpdatePriority,
+            @Nullable PlaybackState playbackState) {
         synchronized (mLock) {
             FullUserRecord user = getFullUserRecordLocked(record.getUserId());
             if (user == null || !user.mPriorityStack.contains(record)) {
@@ -380,6 +407,27 @@
                 return;
             }
             user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority);
+            notifyActivityManagerWithPlaybackStateChanges(record, playbackState);
+        }
+    }
+
+    private void notifyActivityManagerWithPlaybackStateChanges(
+            MediaSessionRecordImpl record, PlaybackState playbackState) {
+        if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+            return;
+        }
+        ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
+                record.getForegroundServiceDelegationOptions();
+        if (foregroundServiceDelegationOptions == null || playbackState == null) {
+            // This record doesn't support FGS delegation. In practice, this is MediaSession2.
+            return;
+        }
+        if (playbackState.shouldAllowServiceToRunInForeground()) {
+            mActivityManagerInternal.startForegroundServiceDelegate(
+                    foregroundServiceDelegationOptions, /* connection= */ null);
+        } else {
+            mActivityManagerInternal.stopForegroundServiceDelegate(
+                    foregroundServiceDelegationOptions);
         }
     }
 
@@ -543,9 +591,23 @@
         }
 
         session.close();
+        notifyActivityManagerWithSessionDestroyed(session);
         mHandler.postSessionsChanged(session);
     }
 
+    private void notifyActivityManagerWithSessionDestroyed(MediaSessionRecordImpl record) {
+        if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
+            return;
+        }
+        ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
+                record.getForegroundServiceDelegationOptions();
+        if (foregroundServiceDelegationOptions == null) {
+            // This record doesn't support FGS delegation. In practice, this is MediaSession2.
+            return;
+        }
+        mActivityManagerInternal.stopForegroundServiceDelegate(foregroundServiceDelegationOptions);
+    }
+
     void tempAllowlistTargetPkgIfPossible(int targetUid, String targetPackage,
             int callingPid, int callingUid, String callingPackage, String reason) {
         final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 9d151c2..f7210dd 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -16,6 +16,7 @@
 
 package com.android.server.media;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -26,6 +27,7 @@
 import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderInfo;
 import android.media.MediaRoute2ProviderService;
+import android.media.MediaRouter2Utils;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
 import android.os.Bundle;
@@ -39,6 +41,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.media.flags.Flags;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -79,6 +82,10 @@
     @GuardedBy("mRequestLock")
     private volatile SessionCreationRequest mPendingSessionCreationRequest;
 
+    private final Object mTransferLock = new Object();
+    @GuardedBy("mTransferLock")
+    @Nullable private volatile SessionCreationRequest mPendingTransferRequest;
+
     SystemMediaRoute2Provider(Context context, UserHandle user) {
         super(COMPONENT_NAME);
         mIsSystemRouteProvider = true;
@@ -146,17 +153,30 @@
     }
 
     @Override
-    public void requestCreateSession(long requestId, String packageName, String routeId,
-            Bundle sessionHints) {
+    public void requestCreateSession(
+            long requestId,
+            String packageName,
+            String routeId,
+            Bundle sessionHints,
+            @RoutingSessionInfo.TransferReason int transferReason,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName) {
         // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with
         // a route ID different from the default route ID. The service should've filtered.
         if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo);
             return;
         }
-        if (TextUtils.equals(routeId, mSelectedRouteId)) {
-            mCallback.onSessionCreated(this, requestId, mSessionInfos.get(0));
-            return;
+
+        if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+            if (TextUtils.equals(routeId, mSelectedRouteId)) {
+                RoutingSessionInfo currentSessionInfo;
+                synchronized (mLock) {
+                    currentSessionInfo = mSessionInfos.get(0);
+                }
+                mCallback.onSessionCreated(this, requestId, currentSessionInfo);
+                return;
+            }
         }
 
         synchronized (mRequestLock) {
@@ -165,10 +185,23 @@
                 mCallback.onRequestFailed(this, mPendingSessionCreationRequest.mRequestId,
                         MediaRoute2ProviderService.REASON_UNKNOWN_ERROR);
             }
-            mPendingSessionCreationRequest = new SessionCreationRequest(requestId, routeId);
+            mPendingSessionCreationRequest =
+                    new SessionCreationRequest(
+                            requestId,
+                            routeId,
+                            RoutingSessionInfo.TRANSFER_REASON_FALLBACK,
+                            transferInitiatorUserHandle,
+                            transferInitiatorPackageName);
         }
 
-        transferToRoute(requestId, SYSTEM_SESSION_ID, routeId);
+        // Only unprivileged routers call this method, therefore we use TRANSFER_REASON_APP.
+        transferToRoute(
+                requestId,
+                transferInitiatorUserHandle,
+                transferInitiatorPackageName,
+                SYSTEM_SESSION_ID,
+                routeId,
+                transferReason);
     }
 
     @Override
@@ -193,12 +226,31 @@
     }
 
     @Override
-    public void transferToRoute(long requestId, String sessionId, String routeId) {
+    public void transferToRoute(
+            long requestId,
+            @NonNull UserHandle transferInitiatorUserHandle,
+            @NonNull String transferInitiatorPackageName,
+            String sessionId,
+            String routeId,
+            @RoutingSessionInfo.TransferReason int transferReason) {
         if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             // The currently selected route is the default route.
             Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
             return;
         }
+
+        if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+            synchronized (mTransferLock) {
+                mPendingTransferRequest =
+                        new SessionCreationRequest(
+                                requestId,
+                                routeId,
+                                transferReason,
+                                transferInitiatorUserHandle,
+                                transferInitiatorPackageName);
+            }
+        }
+
         MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
         boolean isAvailableDeviceRoute =
                 mDeviceRouteController.getAvailableRoutes().stream()
@@ -218,6 +270,11 @@
             mDeviceRouteController.transferTo(null);
             mBluetoothRouteController.transferTo(routeId);
         }
+
+        if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+                && updateSessionInfosIfNeeded()) {
+            notifySessionInfoUpdated();
+        }
     }
 
     @Override
@@ -322,9 +379,11 @@
             MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
             MediaRoute2Info selectedRoute = selectedDeviceRoute;
             MediaRoute2Info selectedBtRoute = mBluetoothRouteController.getSelectedRoute();
+            List<String> transferableRoutes = new ArrayList<>();
+
             if (selectedBtRoute != null) {
                 selectedRoute = selectedBtRoute;
-                builder.addTransferableRoute(selectedDeviceRoute.getId());
+                transferableRoutes.add(selectedDeviceRoute.getId());
             }
             mSelectedRouteId = selectedRoute.getId();
             mDefaultRoute =
@@ -337,12 +396,54 @@
                 for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) {
                     String routeId = route.getId();
                     if (!mSelectedRouteId.equals(routeId)) {
-                        builder.addTransferableRoute(routeId);
+                        transferableRoutes.add(routeId);
                     }
                 }
             }
             for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) {
-                builder.addTransferableRoute(route.getId());
+                transferableRoutes.add(route.getId());
+            }
+
+            for (String route : transferableRoutes) {
+                builder.addTransferableRoute(route);
+            }
+
+            if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+                int transferReason = RoutingSessionInfo.TRANSFER_REASON_FALLBACK;
+                UserHandle transferInitiatorUserHandle = null;
+                String transferInitiatorPackageName = null;
+
+                if (oldSessionInfo != null
+                        && containsSelectedRouteWithId(oldSessionInfo, selectedRoute.getId())) {
+                    transferReason = oldSessionInfo.getTransferReason();
+                    transferInitiatorUserHandle = oldSessionInfo.getTransferInitiatorUserHandle();
+                    transferInitiatorPackageName = oldSessionInfo.getTransferInitiatorPackageName();
+                }
+
+                synchronized (mTransferLock) {
+                    if (mPendingTransferRequest != null) {
+                        boolean isTransferringToTheSelectedRoute =
+                                mPendingTransferRequest.isTargetRoute(selectedRoute);
+                        boolean canBePotentiallyTransferred =
+                                mPendingTransferRequest.isInsideOfRoutesList(transferableRoutes);
+
+                        if (isTransferringToTheSelectedRoute) {
+                            transferReason = mPendingTransferRequest.mTransferReason;
+                            transferInitiatorUserHandle =
+                                    mPendingTransferRequest.mTransferInitiatorUserHandle;
+                            transferInitiatorPackageName =
+                                    mPendingTransferRequest.mTransferInitiatorPackageName;
+
+                            mPendingTransferRequest = null;
+                        } else if (!canBePotentiallyTransferred) {
+                            mPendingTransferRequest = null;
+                        }
+                    }
+                }
+
+                builder.setTransferReason(transferReason)
+                        .setTransferInitiator(
+                                transferInitiatorUserHandle, transferInitiatorPackageName);
             }
 
             RoutingSessionInfo newSessionInfo = builder.setProviderId(mUniqueId).build();
@@ -424,6 +525,22 @@
         return false;
     }
 
+    private boolean containsSelectedRouteWithId(
+            @Nullable RoutingSessionInfo sessionInfo, @NonNull String selectedRouteId) {
+        if (sessionInfo == null) {
+            return false;
+        }
+
+        List<String> selectedRoutes = sessionInfo.getSelectedRoutes();
+
+        if (selectedRoutes.size() != 1) {
+            throw new IllegalStateException("Selected routes list should contain only 1 route id.");
+        }
+
+        String oldSelectedRouteId = MediaRouter2Utils.getOriginalId(selectedRoutes.get(0));
+        return oldSelectedRouteId != null && oldSelectedRouteId.equals(selectedRouteId);
+    }
+
     void publishProviderState() {
         updateProviderState();
         notifyProviderState();
@@ -452,12 +569,47 @@
     }
 
     private static class SessionCreationRequest {
-        final long mRequestId;
-        final String mRouteId;
+        private final long mRequestId;
+        @NonNull private final String mRouteId;
 
-        SessionCreationRequest(long requestId, String routeId) {
-            this.mRequestId = requestId;
-            this.mRouteId = routeId;
+        @RoutingSessionInfo.TransferReason private final int mTransferReason;
+
+        @NonNull private final UserHandle mTransferInitiatorUserHandle;
+        @NonNull private final String mTransferInitiatorPackageName;
+
+        SessionCreationRequest(
+                long requestId,
+                @NonNull String routeId,
+                @RoutingSessionInfo.TransferReason int transferReason,
+                @NonNull UserHandle transferInitiatorUserHandle,
+                @NonNull String transferInitiatorPackageName) {
+            mRequestId = requestId;
+            mRouteId = routeId;
+            mTransferReason = transferReason;
+            mTransferInitiatorUserHandle = transferInitiatorUserHandle;
+            mTransferInitiatorPackageName = transferInitiatorPackageName;
+        }
+
+        private boolean isTargetRoute(@Nullable MediaRoute2Info route2Info) {
+            if (route2Info == null) {
+                return false;
+            }
+
+            return isTargetRoute(route2Info.getId());
+        }
+
+        private boolean isTargetRoute(@Nullable String routeId) {
+            return mRouteId.equals(routeId);
+        }
+
+        private boolean isInsideOfRoutesList(@NonNull List<String> routesList) {
+            for (String routeId : routesList) {
+                if (isTargetRoute(routeId)) {
+                    return true;
+                }
+            }
+
+            return false;
         }
     }
 
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 46e7041..e4e48bd 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4090,6 +4090,7 @@
                 }
                 fout.decreaseIndent();
 
+                fout.println();
                 fout.println("Admin restricted uids for metered data:");
                 fout.increaseIndent();
                 size = mMeteredRestrictedUids.size();
@@ -4099,6 +4100,7 @@
                 }
                 fout.decreaseIndent();
 
+                fout.println();
                 fout.println("Network to interfaces:");
                 fout.increaseIndent();
                 for (int i = 0; i < mNetworkToIfaces.size(); ++i) {
@@ -4108,6 +4110,10 @@
                 fout.decreaseIndent();
 
                 fout.println();
+                fout.print("Active notifications: ");
+                fout.println(mActiveNotifs);
+
+                fout.println();
                 mStatLogger.dump(fout);
 
                 mLogger.dumpLogs(fout);
@@ -6672,7 +6678,7 @@
          * Build unique tag that identifies an active {@link NetworkPolicy}
          * notification of a specific type, like {@link #TYPE_LIMIT}.
          */
-        private String buildNotificationTag(NetworkPolicy policy, int type) {
+        private static String buildNotificationTag(NetworkPolicy policy, int type) {
             return TAG + ":" + policy.template.hashCode() + ":" + type;
         }
 
@@ -6683,5 +6689,10 @@
         public int getId() {
             return mId;
         }
+
+        @Override
+        public String toString() {
+            return mTag;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 2da1a68..66e61c0 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -234,7 +234,7 @@
             if (pkgList != null && (pkgList.length > 0)) {
                 for (String pkgName : pkgList) {
                     try {
-                        inm.removeAutomaticZenRules(pkgName);
+                        inm.removeAutomaticZenRules(pkgName, /* fromUser= */ false);
                         inm.setNotificationPolicyAccessGranted(pkgName, false);
                     } catch (Exception e) {
                         Slog.e(TAG, "Failed to clean up rules for " + pkgName, e);
diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
index 8855666..71a6b5e 100644
--- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
+++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
@@ -109,7 +109,6 @@
         if (origin == ZenModeConfig.UPDATE_ORIGIN_INIT
                 || origin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER
                 || origin == ZenModeConfig.UPDATE_ORIGIN_USER
-                || origin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
                 || !mPowerManager.isInteractive()) {
             unregisterScreenOffReceiver();
             updateNightModeImmediately(useNightMode);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 75d3dce..ff415c1 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5343,13 +5343,14 @@
         }
 
         @Override
-        public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException {
+        public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) {
             enforceSystemOrSystemUI("INotificationManager.setZenMode");
             final int callingUid = Binder.getCallingUid();
             final long identity = Binder.clearCallingIdentity();
+            enforceUserOriginOnlyFromSystem(fromUser, "setZenMode");
+
             try {
-                mZenModeHelper.setManualZenMode(mode, conditionId,
-                        ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, // Checked by enforce()
+                mZenModeHelper.setManualZenMode(mode, conditionId, computeZenOrigin(fromUser),
                         reason, /* caller= */ null, callingUid);
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -5380,7 +5381,8 @@
         }
 
         @Override
-        public String addAutomaticZenRule(AutomaticZenRule automaticZenRule, String pkg) {
+        public String addAutomaticZenRule(AutomaticZenRule automaticZenRule, String pkg,
+                boolean fromUser) {
             validateAutomaticZenRule(automaticZenRule);
             checkCallerIsSameApp(pkg);
             if (automaticZenRule.getZenPolicy() != null
@@ -5389,6 +5391,7 @@
                         + "INTERRUPTION_FILTER_PRIORITY filters");
             }
             enforcePolicyAccess(Binder.getCallingUid(), "addAutomaticZenRule");
+            enforceUserOriginOnlyFromSystem(fromUser, "addAutomaticZenRule");
 
             // If the calling app is the system (from any user), take the package name from the
             // rule's owner rather than from the caller's package.
@@ -5400,24 +5403,18 @@
             }
 
             return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule,
-                    // TODO: b/308670715: Distinguish origin properly (e.g. USER if creating a rule
-                    //  manually in Settings).
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
-                    "addAutomaticZenRule", Binder.getCallingUid());
+                    computeZenOrigin(fromUser), "addAutomaticZenRule", Binder.getCallingUid());
         }
 
         @Override
-        public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
+        public boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule,
+                boolean fromUser) throws RemoteException {
             validateAutomaticZenRule(automaticZenRule);
             enforcePolicyAccess(Binder.getCallingUid(), "updateAutomaticZenRule");
+            enforceUserOriginOnlyFromSystem(fromUser, "updateAutomaticZenRule");
 
-            // TODO: b/308670715: Distinguish origin properly (e.g. USER if updating a rule
-            //  manually in Settings).
             return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
-                    "updateAutomaticZenRule", Binder.getCallingUid());
+                    computeZenOrigin(fromUser), "updateAutomaticZenRule", Binder.getCallingUid());
         }
 
         private void validateAutomaticZenRule(AutomaticZenRule rule) {
@@ -5445,27 +5442,24 @@
         }
 
         @Override
-        public boolean removeAutomaticZenRule(String id) throws RemoteException {
+        public boolean removeAutomaticZenRule(String id, boolean fromUser) throws RemoteException {
             Objects.requireNonNull(id, "Id is null");
             // Verify that they can modify zen rules.
             enforcePolicyAccess(Binder.getCallingUid(), "removeAutomaticZenRule");
+            enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRule");
 
-            // TODO: b/308670715: Distinguish origin properly (e.g. USER if removing a rule
-            //  manually in Settings).
-            return mZenModeHelper.removeAutomaticZenRule(id,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
+            return mZenModeHelper.removeAutomaticZenRule(id, computeZenOrigin(fromUser),
                     "removeAutomaticZenRule", Binder.getCallingUid());
         }
 
         @Override
-        public boolean removeAutomaticZenRules(String packageName) throws RemoteException {
+        public boolean removeAutomaticZenRules(String packageName, boolean fromUser)
+                throws RemoteException {
             Objects.requireNonNull(packageName, "Package name is null");
             enforceSystemOrSystemUI("removeAutomaticZenRules");
+            enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRules");
 
-            return mZenModeHelper.removeAutomaticZenRules(packageName,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
+            return mZenModeHelper.removeAutomaticZenRules(packageName, computeZenOrigin(fromUser),
                     packageName + "|removeAutomaticZenRules", Binder.getCallingUid());
         }
 
@@ -5484,22 +5478,41 @@
             condition.validate();
 
             enforcePolicyAccess(Binder.getCallingUid(), "setAutomaticZenRuleState");
+            boolean fromUser = (condition.source == Condition.SOURCE_USER_ACTION);
 
-            // TODO: b/308670715: Distinguish origin properly (e.g. USER if toggling a rule
-            //  manually in Settings).
-            mZenModeHelper.setAutomaticZenRuleState(id, condition,
-                    isCallerSystemOrSystemUi() ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                            : ZenModeConfig.UPDATE_ORIGIN_APP,
+            mZenModeHelper.setAutomaticZenRuleState(id, condition, computeZenOrigin(fromUser),
                     Binder.getCallingUid());
         }
 
+        @ZenModeConfig.ConfigChangeOrigin
+        private int computeZenOrigin(boolean fromUser) {
+            // "fromUser" is introduced with MODES_API, so only consider it in that case.
+            // (Non-MODES_API behavior should also not depend at all on UPDATE_ORIGIN_USER).
+            if (android.app.Flags.modesApi() && fromUser) {
+                return ZenModeConfig.UPDATE_ORIGIN_USER;
+            } else if (isCallerSystemOrSystemUi()) {
+                return ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI;
+            } else {
+                return ZenModeConfig.UPDATE_ORIGIN_APP;
+            }
+        }
+
+        private void enforceUserOriginOnlyFromSystem(boolean fromUser, String method) {
+            if (android.app.Flags.modesApi()
+                    && fromUser
+                    && !isCallerSystemOrSystemUiOrShell()) {
+                throw new SecurityException(TextUtils.formatSimple(
+                        "Calling %s with fromUser == true is only allowed for system", method));
+            }
+        }
+
         @Override
-        public void setInterruptionFilter(String pkg, int filter) throws RemoteException {
+        public void setInterruptionFilter(String pkg, int filter, boolean fromUser) {
             enforcePolicyAccess(pkg, "setInterruptionFilter");
             final int zen = NotificationManager.zenModeFromInterruptionFilter(filter, -1);
             if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter);
             final int callingUid = Binder.getCallingUid();
-            final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
+            enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter");
 
             if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
                 mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen);
@@ -5508,9 +5521,7 @@
 
             final long identity = Binder.clearCallingIdentity();
             try {
-                mZenModeHelper.setManualZenMode(zen, null,
-                        isSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                                : ZenModeConfig.UPDATE_ORIGIN_APP,
+                mZenModeHelper.setManualZenMode(zen, null, computeZenOrigin(fromUser),
                         /* reason= */ "setInterruptionFilter", /* caller= */ pkg,
                         callingUid);
             } finally {
@@ -5599,7 +5610,8 @@
             return !isCompatChangeEnabled
                     || isCallerSystemOrSystemUi()
                     || hasCompanionDevice(callingPkg, UserHandle.getUserId(callingUid),
-                            AssociationRequest.DEVICE_PROFILE_WATCH);
+                            Set.of(AssociationRequest.DEVICE_PROFILE_WATCH,
+                                    AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION));
         }
 
         private void enforcePolicyAccess(String pkg, String method) {
@@ -5825,10 +5837,11 @@
          * {@link Policy#PRIORITY_CATEGORY_MEDIA} from bypassing dnd
          */
         @Override
-        public void setNotificationPolicy(String pkg, Policy policy) {
+        public void setNotificationPolicy(String pkg, Policy policy, boolean fromUser) {
             enforcePolicyAccess(pkg, "setNotificationPolicy");
+            enforceUserOriginOnlyFromSystem(fromUser, "setNotificationPolicy");
             int callingUid = Binder.getCallingUid();
-            boolean isSystemOrSystemUi = isCallerSystemOrSystemUi();
+            @ZenModeConfig.ConfigChangeOrigin int origin = computeZenOrigin(fromUser);
 
             boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
                     && !canManageGlobalZenPolicy(pkg, callingUid);
@@ -5873,14 +5886,12 @@
                         newVisualEffects, policy.priorityConversationSenders);
 
                 if (shouldApplyAsImplicitRule) {
-                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
+                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy,
+                            origin);
                 } else {
                     ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion,
                             policy);
-                    mZenModeHelper.setNotificationPolicy(policy,
-                            isSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                                    : ZenModeConfig.UPDATE_ORIGIN_APP,
-                            callingUid);
+                    mZenModeHelper.setNotificationPolicy(policy, origin, callingUid);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to set notification policy", e);
@@ -7001,12 +7012,14 @@
             return false;
         }
 
-        final boolean hasBitmap = n.extras.containsKey(Notification.EXTRA_PICTURE);
+        final boolean hasBitmap = n.extras.containsKey(Notification.EXTRA_PICTURE)
+                && n.extras.getParcelable(Notification.EXTRA_PICTURE) != null;
         if (hasBitmap) {
             return true;
         }
 
-        final boolean hasIcon = n.extras.containsKey(Notification.EXTRA_PICTURE_ICON);
+        final boolean hasIcon = n.extras.containsKey(Notification.EXTRA_PICTURE_ICON)
+                && n.extras.getParcelable(Notification.EXTRA_PICTURE_ICON) != null;
         if (hasIcon) {
             return true;
         }
@@ -7022,9 +7035,10 @@
         if (!isBigPictureWithBitmapOrIcon(r.getNotification())) {
             return;
         }
-        // Remove Notification object's reference to picture bitmap or URI
-        r.getNotification().extras.remove(Notification.EXTRA_PICTURE);
-        r.getNotification().extras.remove(Notification.EXTRA_PICTURE_ICON);
+        // Remove Notification object's reference to picture bitmap or URI. Leave the extras set to
+        // null to avoid crashing apps that came to expect them to be present but null.
+        r.getNotification().extras.putParcelable(Notification.EXTRA_PICTURE, null);
+        r.getNotification().extras.putParcelable(Notification.EXTRA_PICTURE_ICON, null);
 
         // Make Notification silent
         r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
@@ -10733,6 +10747,14 @@
             final String key = record.getSbn().getKey();
             final NotificationListenerService.Ranking ranking =
                     new NotificationListenerService.Ranking();
+            ArrayList<Notification.Action> smartActions = record.getSystemGeneratedSmartActions();
+            ArrayList<CharSequence> smartReplies = record.getSmartReplies();
+            if (redactSensitiveNotificationsFromUntrustedListeners()
+                    && !mListeners.isUidTrusted(info.uid)
+                    && mListeners.hasSensitiveContent(record)) {
+                smartActions = null;
+                smartReplies = null;
+            }
             ranking.populate(
                     key,
                     rankings.size(),
@@ -10750,8 +10772,8 @@
                     record.isHidden(),
                     record.getLastAudiblyAlertedMs(),
                     record.getSound() != null || record.getVibration() != null,
-                    record.getSystemGeneratedSmartActions(),
-                    record.getSmartReplies(),
+                    smartActions,
+                    smartReplies,
                     record.canBubble(),
                     record.isTextChanged(),
                     record.isConversation(),
@@ -10780,7 +10802,7 @@
     }
 
     private boolean hasCompanionDevice(String pkg, @UserIdInt int userId,
-            @Nullable @AssociationRequest.DeviceProfile String withDeviceProfile) {
+            @Nullable Set</* @AssociationRequest.DeviceProfile */ String> withDeviceProfiles) {
         if (mCompanionManager == null) {
             mCompanionManager = getCompanionManager();
         }
@@ -10792,7 +10814,7 @@
         try {
             List<AssociationInfo> associations = mCompanionManager.getAssociations(pkg, userId);
             for (AssociationInfo association : associations) {
-                if (withDeviceProfile == null || withDeviceProfile.equals(
+                if (withDeviceProfiles == null || withDeviceProfiles.contains(
                         association.getDeviceProfile())) {
                     return true;
                 }
@@ -11501,20 +11523,16 @@
             super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet);
             String pkgName = getPackageName(pkgOrComponent);
             if (redactSensitiveNotificationsFromUntrustedListeners()) {
-                try {
-                    int uid = mPackageManagerClient.getPackageUidAsUser(pkgName, userId);
-                    if (!enabled) {
-                        synchronized (mTrustedListenerUids) {
-                            mTrustedListenerUids.remove(uid);
-                        }
+                int uid = mPackageManagerInternal.getPackageUid(pkgName, 0, userId);
+                if (!enabled && uid >= 0) {
+                    synchronized (mTrustedListenerUids) {
+                        mTrustedListenerUids.remove(uid);
                     }
-                    if (enabled && isAppTrustedNotificationListenerService(uid, pkgName)) {
-                        synchronized (mTrustedListenerUids) {
-                            mTrustedListenerUids.add(uid);
-                        }
+                }
+                if (enabled && uid >= 0 && isAppTrustedNotificationListenerService(uid, pkgName)) {
+                    synchronized (mTrustedListenerUids) {
+                        mTrustedListenerUids.add(uid);
                     }
-                } catch (NameNotFoundException e) {
-                    Slog.e(TAG, "PackageManager could not find package " + pkgName, e);
                 }
             }
 
@@ -11934,8 +11952,10 @@
 
                 for (final ManagedServiceInfo info : getServices()) {
                     boolean isTrusted = isUidTrusted(info.uid);
-                    boolean sendRedacted = isNewSensitive && !isTrusted;
-                    boolean sendOldRedacted = isOldSensitive && !isTrusted;
+                    boolean sendRedacted = redactSensitiveNotificationsFromUntrustedListeners()
+                            && isNewSensitive && !isTrusted;
+                    boolean sendOldRedacted = redactSensitiveNotificationsFromUntrustedListeners()
+                            && isOldSensitive && !isTrusted;
                     boolean sbnVisible = isVisibleToListener(sbn, r.getNotificationType(), info);
                     boolean oldSbnVisible = (oldSbn != null)
                             && isVisibleToListener(oldSbn, old.getNotificationType(), info);
@@ -12034,7 +12054,7 @@
 
         StatusBarNotification redactStatusBarNotification(StatusBarNotification sbn) {
             if (!redactSensitiveNotificationsFromUntrustedListeners()) {
-                return sbn;
+                throw new RuntimeException("redactStatusBarNotification called while flag is off");
             }
 
             ApplicationInfo appInfo = sbn.getNotification().extras.getParcelable(
@@ -12206,6 +12226,7 @@
         public void notifyRankingUpdateLocked(List<NotificationRecord> changedHiddenNotifications) {
             boolean isHiddenRankingUpdate = changedHiddenNotifications != null
                     && changedHiddenNotifications.size() > 0;
+
             // TODO (b/73052211): if the ranking update changed the notification type,
             // cancel notifications for NLSes that can't see them anymore
             for (final ManagedServiceInfo serviceInfo : getServices()) {
@@ -12229,7 +12250,6 @@
                 if (notifyThisListener || !isHiddenRankingUpdate) {
                     final NotificationRankingUpdate update = makeRankingUpdateLocked(
                             serviceInfo);
-
                     mHandler.post(() -> notifyRankingUpdate(serviceInfo, update));
                 }
             }
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
index dc0cf4e..9f3104c 100644
--- a/services/core/java/com/android/server/notification/NotificationShellCmd.java
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -117,7 +117,6 @@
     private final NotificationManagerService mDirectService;
     private final INotificationManager mBinderService;
     private final PackageManager mPm;
-    private NotificationChannel mChannel;
 
     public NotificationShellCmd(NotificationManagerService service) {
         mDirectService = service;
@@ -183,7 +182,13 @@
                             interruptionFilter = INTERRUPTION_FILTER_ALL;
                     }
                     final int filter = interruptionFilter;
-                    mBinderService.setInterruptionFilter(callingPackage, filter);
+                    if (android.app.Flags.modesApi()) {
+                        mBinderService.setInterruptionFilter(callingPackage, filter,
+                                /* fromUser= */ true);
+                    } else {
+                        mBinderService.setInterruptionFilter(callingPackage, filter,
+                                /* fromUser= */ false);
+                    }
                 }
                 break;
                 case "allow_dnd": {
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index 87158cd..df570a0 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -29,6 +29,7 @@
 import android.os.Process;
 import android.service.notification.DNDPolicyProto;
 import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.util.ArrayMap;
@@ -58,7 +59,7 @@
     // mode change.
     ZenModeEventLogger.ZenStateChanges mChangeState = new ZenModeEventLogger.ZenStateChanges();
 
-    private PackageManager mPm;
+    private final PackageManager mPm;
 
     ZenModeEventLogger(PackageManager pm) {
         mPm = pm;
@@ -97,11 +98,11 @@
      * @param newInfo     ZenModeInfo after this change takes effect
      * @param callingUid  the calling UID associated with the change; may be used to attribute the
      *                    change to a particular package or determine if this is a user action
-     * @param fromSystemOrSystemUi whether the calling UID is either system UID or system UI
+     * @param origin      The origin of the Zen change.
      */
     public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
-            boolean fromSystemOrSystemUi) {
-        mChangeState.init(prevInfo, newInfo, callingUid, fromSystemOrSystemUi);
+            @ConfigChangeOrigin int origin) {
+        mChangeState.init(prevInfo, newInfo, callingUid, origin);
         if (mChangeState.shouldLogChanges()) {
             maybeReassignCallingUid();
             logChanges();
@@ -124,7 +125,7 @@
         // We don't consider the manual rule in the old config because if a manual rule is turning
         // off with a call from system, that could easily be a user action to explicitly turn it off
         if (mChangeState.getChangedRuleType() == RULE_TYPE_MANUAL) {
-            if (!mChangeState.mFromSystemOrSystemUi
+            if (!mChangeState.isFromSystemOrSystemUi()
                     || mChangeState.getNewManualRuleEnabler() == null) {
                 return;
             }
@@ -136,7 +137,7 @@
         //   - we've determined it's not a user action
         //   - our current best guess is that the calling uid is system/sysui
         if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) {
-            if (mChangeState.getIsUserAction() || !mChangeState.mFromSystemOrSystemUi) {
+            if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) {
                 return;
             }
 
@@ -221,10 +222,10 @@
         ZenModeConfig mPrevConfig, mNewConfig;
         NotificationManager.Policy mPrevPolicy, mNewPolicy;
         int mCallingUid = Process.INVALID_UID;
-        boolean mFromSystemOrSystemUi = false;
+        @ConfigChangeOrigin int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
 
         private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
-                boolean fromSystemOrSystemUi) {
+                @ConfigChangeOrigin int origin) {
             // previous & new may be the same -- that would indicate that zen mode hasn't changed.
             mPrevZenMode = prevInfo.mZenMode;
             mNewZenMode = newInfo.mZenMode;
@@ -233,7 +234,7 @@
             mPrevPolicy = prevInfo.mPolicy;
             mNewPolicy = newInfo.mPolicy;
             mCallingUid = callingUid;
-            mFromSystemOrSystemUi = fromSystemOrSystemUi;
+            mOrigin = origin;
         }
 
         /**
@@ -389,12 +390,16 @@
 
         /**
          * Return our best guess as to whether the changes observed are due to a user action.
-         * Note that this won't be 100% accurate as we can't necessarily distinguish between a
-         * system uid call indicating "user interacted with Settings" vs "a system app changed
-         * something automatically".
+         * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily
+         * distinguish between a system uid call indicating "user interacted with Settings" vs "a
+         * system app changed something automatically".
          */
         boolean getIsUserAction() {
-            // Approach:
+            if (Flags.modesApi()) {
+                return mOrigin == ZenModeConfig.UPDATE_ORIGIN_USER;
+            }
+
+            // Approach for pre-MODES_API:
             //   - if manual rule turned on or off, the calling UID is system, and the new manual
             //     rule does not have an enabler set, guess that this is likely to be a user action.
             //     This may represent a system app turning on DND automatically, but we guess "user"
@@ -419,13 +424,13 @@
             switch (getChangedRuleType()) {
                 case RULE_TYPE_MANUAL:
                     // TODO(b/278888961): Distinguish the automatically-turned-off state
-                    return mFromSystemOrSystemUi && (getNewManualRuleEnabler() == null);
+                    return isFromSystemOrSystemUi() && (getNewManualRuleEnabler() == null);
                 case RULE_TYPE_AUTOMATIC:
                     for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) {
                         if (d.wasAdded() || d.wasRemoved()) {
                             // If the change comes from system, a rule being added/removed indicates
                             // a likely user action. From an app, it's harder to know for sure.
-                            return mFromSystemOrSystemUi;
+                            return isFromSystemOrSystemUi();
                         }
                         ZenModeDiff.FieldDiff enabled = d.getDiffForField(
                                 ZenModeDiff.RuleDiff.FIELD_ENABLED);
@@ -455,6 +460,13 @@
             return false;
         }
 
+        boolean isFromSystemOrSystemUi() {
+            return mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT
+                    || mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER
+                    || mOrigin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
+                    || mOrigin == ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP;
+        }
+
         /**
          * Get the package UID associated with this change, which is just the calling UID for the
          * relevant method changes. This may get reset by ZenModeEventLogger, which has access to
@@ -612,7 +624,7 @@
             copy.mPrevPolicy = mPrevPolicy.copy();
             copy.mNewPolicy = mNewPolicy.copy();
             copy.mCallingUid = mCallingUid;
-            copy.mFromSystemOrSystemUi = mFromSystemOrSystemUi;
+            copy.mOrigin = mOrigin;
             return copy;
         }
     }
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 3f8b595..0a46901 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -561,7 +561,7 @@
      * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}.
      */
     void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid,
-            NotificationManager.Policy policy) {
+            NotificationManager.Policy policy, @ConfigChangeOrigin int origin) {
         if (!android.app.Flags.modesApi()) {
             Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
             return;
@@ -579,7 +579,7 @@
             }
             // TODO: b/308673679 - Keep user customization of this rule!
             rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
-            setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
+            setConfigLocked(newConfig, /* triggeringComponent= */ null, origin,
                     "applyGlobalPolicyAsImplicitZenRule", callingUid);
         }
     }
@@ -1371,12 +1371,8 @@
         if (logZenModeEvents) {
             ZenModeEventLogger.ZenModeInfo newInfo = new ZenModeEventLogger.ZenModeInfo(
                     mZenMode, mConfig, mConsolidatedPolicy);
-            boolean fromSystemOrSystemUi = origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
-                    || origin == UPDATE_ORIGIN_INIT
-                    || origin == UPDATE_ORIGIN_INIT_USER
-                    || origin == UPDATE_ORIGIN_RESTORE_BACKUP;
             mZenModeEventLogger.maybeLogZenChange(prevInfo, newInfo, callingUid,
-                    fromSystemOrSystemUi);
+                    origin);
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index da01745..8e79922a 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -33,4 +33,20 @@
   namespace: "systemui"
   description: "This flag controls the vibrate while unlocked setting of polite notifications"
   bug: "270456865"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "autogroup_summary_icon_update"
+  namespace: "systemui"
+  description: "This flag controls the fix for notifications autogroup summary icon updates"
+  bug: "227693160"
+}
+
+flag {
+  name: "sensitive_notification_app_protection"
+  namespace: "systemui"
+  description: "This flag controls the sensitive notification app protections while screen sharing"
+  bug: "312784351"
+  # Referenced in WM where WM starts before DeviceConfig
+  is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java
index 9ce3cb3..f6e7ef3 100644
--- a/services/core/java/com/android/server/os/NativeTombstoneManager.java
+++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java
@@ -41,13 +41,14 @@
 import android.system.StructStat;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoParseException;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.BootReceiver;
 import com.android.server.ServiceThread;
 import com.android.server.os.TombstoneProtos.Cause;
 import com.android.server.os.TombstoneProtos.Tombstone;
-import com.android.server.os.protobuf.CodedInputStream;
 
 import libcore.io.IoUtils;
 
@@ -129,21 +130,18 @@
             return;
         }
 
+        String processName = "UNKNOWN";
         final boolean isProtoFile = filename.endsWith(".pb");
-        if (!isProtoFile) {
-            return;
-        }
+        File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb");
 
-        Optional<ParsedTombstone> parsedTombstone = handleProtoTombstone(path, true);
+        Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile);
         if (parsedTombstone.isPresent()) {
-            BootReceiver.addTombstoneToDropBox(
-                    mContext, path, parsedTombstone.get().getTombstone(),
-                    parsedTombstone.get().getProcessName(), mTmpFileLock);
+            processName = parsedTombstone.get().getProcessName();
         }
+        BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock);
     }
 
-    private Optional<ParsedTombstone> handleProtoTombstone(
-            File path, boolean addToList) {
+    private Optional<TombstoneFile> handleProtoTombstone(File path, boolean addToList) {
         final String filename = path.getName();
         if (!filename.endsWith(".pb")) {
             Slog.w(TAG, "unexpected tombstone name: " + path);
@@ -173,7 +171,7 @@
             return Optional.empty();
         }
 
-        final Optional<ParsedTombstone> parsedTombstone = TombstoneFile.parse(pfd);
+        final Optional<TombstoneFile> parsedTombstone = TombstoneFile.parse(pfd);
         if (!parsedTombstone.isPresent()) {
             IoUtils.closeQuietly(pfd);
             return Optional.empty();
@@ -186,7 +184,7 @@
                     previous.dispose();
                 }
 
-                mTombstones.put(number, parsedTombstone.get().getTombstoneFile());
+                mTombstones.put(number, parsedTombstone.get());
             }
         }
 
@@ -334,27 +332,6 @@
         }
     }
 
-    static class ParsedTombstone {
-        TombstoneFile mTombstoneFile;
-        Tombstone mTombstone;
-        ParsedTombstone(TombstoneFile tombstoneFile, Tombstone tombstone) {
-            mTombstoneFile = tombstoneFile;
-            mTombstone = tombstone;
-        }
-
-        public String getProcessName() {
-            return mTombstoneFile.getProcessName();
-        }
-
-        public TombstoneFile getTombstoneFile() {
-            return mTombstoneFile;
-        }
-
-        public Tombstone getTombstone() {
-            return mTombstone;
-        }
-    }
-
     static class TombstoneFile {
         final ParcelFileDescriptor mPfd;
 
@@ -437,21 +414,67 @@
             }
         }
 
-        static Optional<ParsedTombstone> parse(ParcelFileDescriptor pfd) {
-            Tombstone tombstoneProto;
-            try (FileInputStream is = new FileInputStream(pfd.getFileDescriptor())) {
-                final byte[] tombstoneBytes = is.readAllBytes();
+        static Optional<TombstoneFile> parse(ParcelFileDescriptor pfd) {
+            final FileInputStream is = new FileInputStream(pfd.getFileDescriptor());
+            final ProtoInputStream stream = new ProtoInputStream(is);
 
-                tombstoneProto = Tombstone.parseFrom(
-                        CodedInputStream.newInstance(tombstoneBytes));
-            } catch (IOException ex) {
+            int pid = 0;
+            int uid = 0;
+            String processName = null;
+            String crashReason = "";
+            String selinuxLabel = "";
+
+            try {
+                while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (stream.getFieldNumber()) {
+                        case (int) Tombstone.PID:
+                            pid = stream.readInt(Tombstone.PID);
+                            break;
+
+                        case (int) Tombstone.UID:
+                            uid = stream.readInt(Tombstone.UID);
+                            break;
+
+                        case (int) Tombstone.COMMAND_LINE:
+                            if (processName == null) {
+                                processName = stream.readString(Tombstone.COMMAND_LINE);
+                            }
+                            break;
+
+                        case (int) Tombstone.CAUSES:
+                            if (!crashReason.equals("")) {
+                                // Causes appear in decreasing order of likelihood. For now we only
+                                // want the most likely crash reason here, so ignore all others.
+                                break;
+                            }
+                            long token = stream.start(Tombstone.CAUSES);
+                        cause:
+                            while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                                switch (stream.getFieldNumber()) {
+                                    case (int) Cause.HUMAN_READABLE:
+                                        crashReason = stream.readString(Cause.HUMAN_READABLE);
+                                        break cause;
+
+                                    default:
+                                        break;
+                                }
+                            }
+                            stream.end(token);
+                            break;
+
+                        case (int) Tombstone.SELINUX_LABEL:
+                            selinuxLabel = stream.readString(Tombstone.SELINUX_LABEL);
+                            break;
+
+                        default:
+                            break;
+                    }
+                }
+            } catch (IOException | ProtoParseException ex) {
                 Slog.e(TAG, "Failed to parse tombstone", ex);
                 return Optional.empty();
             }
 
-            int pid = tombstoneProto.getPid();
-            int uid = tombstoneProto.getUid();
-
             if (!UserHandle.isApp(uid)) {
                 Slog.e(TAG, "Tombstone's UID (" + uid + ") not an app, ignoring");
                 return Optional.empty();
@@ -468,7 +491,6 @@
             final int userId = UserHandle.getUserId(uid);
             final int appId = UserHandle.getAppId(uid);
 
-            String selinuxLabel = tombstoneProto.getSelinuxLabel();
             if (!selinuxLabel.startsWith("u:r:untrusted_app")) {
                 Slog.e(TAG, "Tombstone has invalid selinux label (" + selinuxLabel + "), ignoring");
                 return Optional.empty();
@@ -480,30 +502,11 @@
             result.mAppId = appId;
             result.mPid = pid;
             result.mUid = uid;
-            result.mProcessName = getCmdLineProcessName(tombstoneProto);
+            result.mProcessName = processName == null ? "" : processName;
             result.mTimestampMs = timestampMs;
-            result.mCrashReason = getCrashReason(tombstoneProto);
+            result.mCrashReason = crashReason;
 
-            return Optional.of(new ParsedTombstone(result, tombstoneProto));
-        }
-
-        private static String getCmdLineProcessName(Tombstone tombstoneProto) {
-            for (String cmdline : tombstoneProto.getCommandLineList()) {
-                if (cmdline != null) {
-                    return cmdline;
-                }
-            }
-            return "";
-        }
-
-        private static String getCrashReason(Tombstone tombstoneProto) {
-            for (Cause cause : tombstoneProto.getCausesList()) {
-                if (cause.getHumanReadable() != null
-                        && !cause.getHumanReadable().equals("")) {
-                    return cause.getHumanReadable();
-                }
-            }
-            return "";
+            return Optional.of(result);
         }
 
         public IParcelFileDescriptorRetriever getPfdRetriever() {
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index 82622d9..f3df424 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -1042,7 +1042,9 @@
                 existingSettings, forceQueryable, protectedBroadcasts);
         SparseSetArray<Integer> queriesViaComponent = computer.execute();
         synchronized (mQueriesViaComponentLock) {
-            mQueriesViaComponent.copyFrom(queriesViaComponent);
+            mQueriesViaComponent = new WatchedSparseSetArray<>(queriesViaComponent);
+            mQueriesViaComponentSnapshot = new SnapshotCache.Auto<>(
+                    mQueriesViaComponent, mQueriesViaComponent, "AppsFilter.mQueriesViaComponent");
         }
 
         mQueriesViaComponentRequireRecompute.set(false);
diff --git a/services/core/java/com/android/server/pm/CleanUpArgs.java b/services/core/java/com/android/server/pm/CleanUpArgs.java
new file mode 100644
index 0000000..9da76d6
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CleanUpArgs.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.annotation.NonNull;
+
+import java.io.File;
+
+final class CleanUpArgs {
+    @NonNull
+    private final String mPackageName;
+    @NonNull
+    private final File mCodeFile;
+    @NonNull
+    private final String[] mInstructionSets;
+
+    /**
+     * Create args that describe an existing installed package. Typically used
+     * when cleaning up old installs.
+     */
+    CleanUpArgs(@NonNull String packageName, @NonNull String codePath,
+                @NonNull String[] instructionSets) {
+        mPackageName = packageName;
+        mCodeFile = new File(codePath);
+        mInstructionSets = instructionSets;
+    }
+
+    @NonNull
+    String getPackageName() {
+        return mPackageName;
+    }
+
+    @NonNull
+    File getCodeFile() {
+        return mCodeFile;
+    }
+
+    @NonNull
+    /** @see PackageSetting#getPath() */
+    String getCodePath() {
+        return mCodeFile.getAbsolutePath();
+    }
+
+    @NonNull
+    String[] getInstructionSets() {
+        return mInstructionSets;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index 27f4e11..482807c 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -518,7 +518,9 @@
     boolean isPackageStoppedForUser(@NonNull String packageName, @UserIdInt int userId)
             throws PackageManager.NameNotFoundException;
 
-    boolean isSuspendingAnyPackages(@NonNull String suspendingPackage, @UserIdInt int userId);
+    /** Check if the package is suspending any package. */
+    boolean isSuspendingAnyPackages(@NonNull String suspendingPackage,
+            @UserIdInt int suspendingUserId, int targetUserId);
 
     @NonNull
     ParceledListSlice<IntentFilter> getAllIntentFilters(@NonNull String packageName);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 744c946..3cb2420 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -96,6 +96,7 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.SigningInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.VersionedPackage;
 import android.os.Binder;
 import android.os.Build;
@@ -5008,11 +5009,13 @@
 
     @Override
     public boolean isSuspendingAnyPackages(@NonNull String suspendingPackage,
-            @UserIdInt int userId) {
+            @UserIdInt int suspendingUserId, int targetUserId) {
+        final UserPackage suspender = UserPackage.of(suspendingUserId, suspendingPackage);
         for (final PackageStateInternal packageState : getPackageStates().values()) {
-            final PackageUserStateInternal state = packageState.getUserStateOrDefault(userId);
+            final PackageUserStateInternal state =
+                    packageState.getUserStateOrDefault(targetUserId);
             if (state.getSuspendParams() != null
-                    && state.getSuspendParams().containsKey(suspendingPackage)) {
+                    && state.getSuspendParams().containsKey(suspender)) {
                 return true;
             }
         }
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index aa7f0d3..b96b704 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -282,8 +282,8 @@
         // other processes clean up before deleting resources.
         synchronized (mPm.mInstallLock) {
             if (info.mArgs != null) {
-                mRemovePackageHelper.cleanUpResources(info.mArgs.mCodeFile,
-                        info.mArgs.mInstructionSets);
+                mRemovePackageHelper.cleanUpResources(info.mArgs.getPackageName(),
+                        info.mArgs.getCodeFile(), info.mArgs.getInstructionSets());
             }
 
             boolean reEnableStub = false;
@@ -571,7 +571,7 @@
 
         // Delete application code and resources only for parent packages
         if (deleteCodeAndResources) {
-            outInfo.mArgs = new InstallArgs(
+            outInfo.mArgs = new CleanUpArgs(ps.getName(),
                     ps.getPathString(), getAppDexInstructionSets(
                             ps.getPrimaryCpuAbiLegacy(), ps.getSecondaryCpuAbiLegacy()));
             if (DEBUG_SD_INSTALL) Slog.i(TAG, "args=" + outInfo.mArgs);
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index 3b9f9c8..41d0176 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -46,11 +46,11 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.om.OverlayConfig;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.EventLogTags;
 import com.android.server.pm.parsing.PackageCacher;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.utils.WatchedArrayMap;
 
diff --git a/services/core/java/com/android/server/pm/InstallArgs.java b/services/core/java/com/android/server/pm/InstallArgs.java
index cc6bb00..46f9732 100644
--- a/services/core/java/com/android/server/pm/InstallArgs.java
+++ b/services/core/java/com/android/server/pm/InstallArgs.java
@@ -16,14 +16,9 @@
 
 package com.android.server.pm;
 
-import static android.app.AppOpsManager.MODE_DEFAULT;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.pm.DataLoaderType;
 import android.content.pm.IPackageInstallObserver2;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageManager;
 import android.content.pm.SigningDetails;
 import android.os.UserHandle;
 import android.util.ArrayMap;
@@ -102,22 +97,4 @@
         mPackageSource = packageSource;
         mApplicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
     }
-
-    /**
-     * Create args that describe an existing installed package. Typically used
-     * when cleaning up old installs, or used as a move source.
-     */
-    InstallArgs(String codePath, String[] instructionSets) {
-        this(OriginInfo.fromNothing(), null, null, 0, 0, InstallSource.EMPTY, null, null,
-                instructionSets, null, new ArrayMap<>(), null, MODE_DEFAULT, null, 0,
-                SigningDetails.UNKNOWN, PackageManager.INSTALL_REASON_UNKNOWN,
-                PackageManager.INSTALL_SCENARIO_DEFAULT, false, DataLoaderType.NONE,
-                PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, false);
-        mCodeFile = (codePath != null) ? new File(codePath) : null;
-    }
-
-    /** @see PackageSettingBase#getPath() */
-    String getCodePath() {
-        return (mCodeFile != null) ? mCodeFile.getAbsolutePath() : null;
-    }
 }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 0fa7aa5..b638d30 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -146,6 +146,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
+import android.util.ExceptionUtils;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
@@ -154,6 +155,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.content.F2fsUtils;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.AndroidPackageLegacyUtils;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
@@ -178,7 +180,6 @@
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.parsing.PackageCacher;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.permission.Permission;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
@@ -672,6 +673,9 @@
                 if (pkgSetting == null || pkgSetting.getPkg() == null) {
                     return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender);
                 }
+                if (instantApp && (pkgSetting.isSystem() || pkgSetting.isUpdatedSystemApp())) {
+                    return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender);
+                }
                 if (!snapshot.canViewInstantApps(callingUid, UserHandle.getUserId(callingUid))) {
                     // only allow the existing package to be used if it's installed as a full
                     // application for at least one user
@@ -1167,8 +1171,9 @@
                         parseFlags);
                 archivedPackage = request.getPackageLite().getArchivedPackage();
             }
-        } catch (PackageManagerException | PackageParserException e) {
-            throw new PrepareFailure("Failed parse during installPackageLI", e);
+        } catch (PackageParserException e) {
+            throw new PrepareFailure(e.error,
+                    ExceptionUtils.getCompleteMessage("Failed parse during installPackageLI", e));
         } finally {
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         }
@@ -2111,7 +2116,8 @@
                         // We didn't need to disable the .apk as a current system package,
                         // which means we are replacing another update that is already
                         // installed.  We need to make sure to delete the older one's .apk.
-                        installRequest.getRemovedInfo().mArgs = new InstallArgs(
+                        installRequest.getRemovedInfo().mArgs = new CleanUpArgs(
+                                packageName,
                                 oldPackage.getPath(),
                                 getAppDexInstructionSets(
                                         deletedPkgSetting.getPrimaryCpuAbi(),
@@ -2820,7 +2826,7 @@
             request.setReturnMessage("Package was removed before install could complete.");
 
             // Remove the update failed package's older resources safely now
-            mRemovePackageHelper.cleanUpResources(request.getOldCodeFile(),
+            mRemovePackageHelper.cleanUpResources(packageName, request.getOldCodeFile(),
                     request.getOldInstructionSet());
             mPm.notifyInstallObserver(request);
             return;
@@ -2857,14 +2863,17 @@
                 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);
-                }
+            // 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) {
+                final int appId = request.getAppId();
+                mPm.mHandler.post(() -> {
+                    for (int userId : firstUserIds) {
+                        enableRestrictedSettings(packageName, appId, userId);
+                    }
+                });
             }
 
             // Log current value of "unknown sources" setting
@@ -2872,7 +2881,7 @@
                     getUnknownSourcesSettings());
 
             // Remove the replaced package's older resources safely now
-            InstallArgs args = request.getRemovedInfo() != null
+            CleanUpArgs args = request.getRemovedInfo() != null
                     ? request.getRemovedInfo().mArgs : null;
             if (args != null) {
                 if (!killApp) {
@@ -2883,7 +2892,8 @@
                     // propagated to all application threads.
                     mPm.scheduleDeferredNoKillPostDelete(args);
                 } else {
-                    mRemovePackageHelper.cleanUpResources(args.mCodeFile, args.mInstructionSets);
+                    mRemovePackageHelper.cleanUpResources(packageName, args.getCodeFile(),
+                            args.getInstructionSets());
                 }
             } else {
                 // Force a gc to clear up things. Ask for a background one, it's fine to go on
@@ -3678,6 +3688,8 @@
         final ParsedPackage parsedPackage;
         try (PackageParser2 pp = mPm.mInjector.getScanningPackageParser()) {
             parsedPackage = pp.parsePackage(scanFile, parseFlags, false);
+        } catch (PackageParserException e) {
+            throw new PackageManagerException(e.error, e.getMessage(), e);
         } finally {
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         }
@@ -4040,7 +4052,7 @@
                             + "; " + pkgSetting.getPathString()
                             + " --> " + parsedPackage.getPath());
 
-            mRemovePackageHelper.cleanUpResources(
+            mRemovePackageHelper.cleanUpResources(pkgSetting.getPackageName(),
                     new File(pkgSetting.getPathString()),
                     getAppDexInstructionSets(pkgSetting.getPrimaryCpuAbiLegacy(),
                             pkgSetting.getSecondaryCpuAbiLegacy()));
@@ -4146,7 +4158,8 @@
                                 + parsedPackage.getLongVersionCode()
                                 + "; " + pkgSetting.getPathString() + " --> "
                                 + parsedPackage.getPath());
-                mRemovePackageHelper.cleanUpResources(new File(pkgSetting.getPathString()),
+                mRemovePackageHelper.cleanUpResources(pkgSetting.getPackageName(),
+                        new File(pkgSetting.getPathString()),
                         getAppDexInstructionSets(
                                 pkgSetting.getPrimaryCpuAbiLegacy(), pkgSetting.getSecondaryCpuAbiLegacy()));
             } else {
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index ee780d9..ebaf516 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -294,13 +294,13 @@
     @Nullable
     public File getOldCodeFile() {
         return (mRemovedInfo != null && mRemovedInfo.mArgs != null)
-                ? mRemovedInfo.mArgs.mCodeFile : null;
+                ? mRemovedInfo.mArgs.getCodeFile() : null;
     }
 
     @Nullable
     public String[] getOldInstructionSet() {
         return (mRemovedInfo != null && mRemovedInfo.mArgs != null)
-                ? mRemovedInfo.mArgs.mInstructionSets : null;
+                ? mRemovedInfo.mArgs.getInstructionSets() : null;
     }
 
     public UserHandle getUser() {
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index 187cada..e970d2c 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -51,8 +51,8 @@
 import com.android.internal.content.F2fsUtils;
 import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.content.NativeLibraryHelper;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.util.Preconditions;
-import com.android.server.pm.parsing.PackageParser2;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 2864a8b..dcfc855d 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -132,6 +132,11 @@
     private static final String EXTRA_INSTALLER_TITLE =
             "com.android.content.pm.extra.UNARCHIVE_INSTALLER_TITLE";
 
+    private static final PorterDuffColorFilter OPACITY_LAYER_FILTER =
+            new PorterDuffColorFilter(
+                    Color.argb(0.5f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */),
+                    PorterDuff.Mode.SRC_ATOP);
+
     private final Context mContext;
     private final PackageManagerService mPm;
 
@@ -746,11 +751,7 @@
             return bitmap;
         }
         BitmapDrawable appIconDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
-        PorterDuffColorFilter colorFilter =
-                new PorterDuffColorFilter(
-                        Color.argb(0.32f /* alpha */, 0f /* red */, 0f /* green */, 0f /* blue */),
-                        PorterDuff.Mode.SRC_ATOP);
-        appIconDrawable.setColorFilter(colorFilter);
+        appIconDrawable.setColorFilter(OPACITY_LAYER_FILTER);
         appIconDrawable.setBounds(
                 0 /* left */,
                 0 /* top */,
diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java
index 4ecbd15..ee5875e 100644
--- a/services/core/java/com/android/server/pm/PackageHandler.java
+++ b/services/core/java/com/android/server/pm/PackageHandler.java
@@ -101,9 +101,10 @@
                 Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "postInstall", msg.arg1);
             } break;
             case DEFERRED_NO_KILL_POST_DELETE: {
-                InstallArgs args = (InstallArgs) msg.obj;
+                CleanUpArgs args = (CleanUpArgs) msg.obj;
                 if (args != null) {
-                    mPm.cleanUpResources(args.mCodeFile, args.mInstructionSets);
+                    mPm.cleanUpResources(args.getPackageName(), args.getCodeFile(),
+                            args.getInstructionSets());
                 }
             } break;
             case DEFERRED_NO_KILL_INSTALL_OBSERVER:
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 2942bbb..cbd65a4 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -111,6 +111,7 @@
 import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.util.ImageUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.TypedXmlPullParser;
@@ -120,7 +121,6 @@
 import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.utils.RequestThrottle;
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index c737b45..8da1683 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -19,6 +19,8 @@
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
 import static android.content.pm.PackageManager.RESTRICTION_NONE;
 
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -37,6 +39,7 @@
 import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.SuspendDialogInfo;
+import android.content.pm.UserPackage;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -250,7 +253,7 @@
         getSuspendPackageHelper().removeSuspensionsBySuspendingPackage(snapshot(),
                 new String[]{packageName},
                 (suspendingPackage) -> !PackageManagerService.PLATFORM_PACKAGE_NAME.equals(
-                        suspendingPackage),
+                        suspendingPackage.packageName),
                 userId);
     }
 
@@ -269,7 +272,7 @@
 
     @Override
     @Deprecated
-    public final String getSuspendingPackage(String suspendedPackage, int userId) {
+    public final UserPackage getSuspendingPackage(String suspendedPackage, int userId) {
         return getSuspendPackageHelper().getSuspendingPackage(snapshot(), suspendedPackage, userId,
                 Binder.getCallingUid());
     }
@@ -277,7 +280,7 @@
     @Override
     @Deprecated
     public final SuspendDialogInfo getSuspendedDialogInfo(String suspendedPackage,
-            String suspendingPackage, int userId) {
+            UserPackage suspendingPackage, int userId) {
         return getSuspendPackageHelper().getSuspendedDialogInfo(snapshot(), suspendedPackage,
                 suspendingPackage, userId, Binder.getCallingUid());
     }
@@ -683,14 +686,16 @@
 
     @Override
     @Deprecated
-    public final void unsuspendForSuspendingPackage(final String packageName, int affectedUser) {
-        mService.unsuspendForSuspendingPackage(snapshot(), packageName, affectedUser);
+    public final void unsuspendAdminSuspendedPackages(int affectedUser) {
+        final int suspendingUserId = affectedUser;
+        mService.unsuspendForSuspendingPackage(snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId);
     }
 
     @Override
     @Deprecated
-    public final boolean isSuspendingAnyPackages(String suspendingPackage, int userId) {
-        return snapshot().isSuspendingAnyPackages(suspendingPackage, userId);
+    public final boolean isAdminSuspendingAnyPackages(int userId) {
+        final int suspendingUserId = userId;
+        return snapshot().isSuspendingAnyPackages(PLATFORM_PACKAGE_NAME, suspendingUserId, userId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 56365b6..b7deef0 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -181,6 +181,7 @@
 import com.android.internal.content.F2fsUtils;
 import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.content.om.OverlayConfig;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.parsing.pkg.AndroidPackageInternal;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.pm.pkg.component.ParsedInstrumentation;
@@ -221,8 +222,8 @@
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DynamicCodeLogger;
 import com.android.server.pm.local.PackageManagerLocalImpl;
+import com.android.server.pm.parsing.PackageCacher;
 import com.android.server.pm.parsing.PackageInfoUtils;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
 import com.android.server.pm.permission.LegacyPermissionManagerService;
@@ -292,6 +293,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * Keep track of all those APKs everywhere.
@@ -605,6 +607,8 @@
     private final boolean mIsUpgrade;
     private final boolean mIsPreNMR1Upgrade;
     private final boolean mIsPreQUpgrade;
+    // If mIsUpgrade == true, contains the prior SDK version, else -1.
+    private final int mPriorSdkVersion;
 
     // Used for privilege escalation. MUST NOT BE CALLED WITH mPackages
     // LOCK HELD.  Can be called with mInstallLock held.
@@ -1293,7 +1297,7 @@
         mHandler.sendMessageDelayed(message, DEFERRED_NO_KILL_INSTALL_OBSERVER_DELAY_MS);
     }
 
-    void scheduleDeferredNoKillPostDelete(InstallArgs args) {
+    void scheduleDeferredNoKillPostDelete(CleanUpArgs args) {
         Message message = mHandler.obtainMessage(DEFERRED_NO_KILL_POST_DELETE, args);
         // If the feature flag is on, retain the old files for a day. Otherwise, delete the old
         // files after a few seconds.
@@ -1697,7 +1701,7 @@
                         () -> LocalServices.getService(UserManagerInternal.class)),
                 (i, pm) -> new DisplayMetrics(),
                 (i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(),
-                        pm.mCacheDir,
+                        new PackageCacher(pm.mCacheDir),
                         pm.mPackageParserCallback) /* scanningCachingPackageParserProducer */,
                 (i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(), null,
                         pm.mPackageParserCallback) /* scanningPackageParserProducer */,
@@ -1889,6 +1893,7 @@
         mInstantAppResolverSettingsComponent = testParams.instantAppResolverSettingsComponent;
         mIsPreNMR1Upgrade = testParams.isPreNmr1Upgrade;
         mIsPreQUpgrade = testParams.isPreQupgrade;
+        mPriorSdkVersion = testParams.priorSdkVersion;
         mIsUpgrade = testParams.isUpgrade;
         mMetrics = testParams.Metrics;
         mModuleInfoProvider = testParams.moduleInfoProvider;
@@ -2228,7 +2233,7 @@
                         "Upgrading from " + ver.fingerprint + " (" + ver.buildFingerprint + ") to "
                                 + PackagePartitions.FINGERPRINT + " (" + Build.FINGERPRINT + ")");
             }
-
+            mPriorSdkVersion = mIsUpgrade ? ver.sdkVersion : -1;
             mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
                     mInjector.getSystemPartitions());
 
@@ -3137,7 +3142,7 @@
     }
 
     private void enforceCanSetPackagesSuspendedAsUser(@NonNull Computer snapshot,
-            boolean quarantined, String callingPackage, int callingUid, int userId,
+            boolean quarantined, UserPackage suspender, int callingUid, int targetUserId,
             String callingMethod) {
         if (callingUid == Process.ROOT_UID
                 // Need to compare app-id to allow system dialogs access on secondary users
@@ -3145,9 +3150,10 @@
             return;
         }
 
-        final String ownerPackage = mProtectedPackages.getDeviceOwnerOrProfileOwnerPackage(userId);
+        final String ownerPackage =
+                mProtectedPackages.getDeviceOwnerOrProfileOwnerPackage(targetUserId);
         if (ownerPackage != null) {
-            final int ownerUid = snapshot.getPackageUid(ownerPackage, 0, userId);
+            final int ownerUid = snapshot.getPackageUid(ownerPackage, 0, targetUserId);
             if (ownerUid == callingUid) {
                 return;
             }
@@ -3168,25 +3174,27 @@
                     callingMethod);
         }
 
-        final int packageUid = snapshot.getPackageUid(callingPackage, 0, userId);
+        final int packageUid = snapshot.getPackageUid(suspender.packageName, 0, targetUserId);
         final boolean allowedPackageUid = packageUid == callingUid;
         // TODO(b/139383163): remove special casing for shell and enforce INTERACT_ACROSS_USERS_FULL
         final boolean allowedShell = callingUid == SHELL_UID
                 && UserHandle.isSameApp(packageUid, callingUid);
 
         if (!allowedShell && !allowedPackageUid) {
-            throw new SecurityException("Calling package " + callingPackage + " in user "
-                    + userId + " does not belong to calling uid " + callingUid);
+            throw new SecurityException("Suspending package " + suspender.packageName
+                    + " in user " + targetUserId + " does not belong to calling uid " + callingUid);
         }
     }
 
     void unsuspendForSuspendingPackage(@NonNull Computer computer, String suspendingPackage,
-            @UserIdInt int userId) {
+            @UserIdInt int suspendingUserId) {
         // TODO: This can be replaced by a special parameter to iterate all packages, rather than
         //  this weird pre-collect of all packages.
         final String[] allPackages = computer.getPackageStates().keySet().toArray(new String[0]);
+        final Predicate<UserPackage> suspenderPredicate =
+                UserPackage.of(suspendingUserId, suspendingPackage)::equals;
         mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer,
-                allPackages, suspendingPackage::equals, userId);
+                allPackages, suspenderPredicate, suspendingUserId);
     }
 
     void removeAllDistractingPackageRestrictions(@NonNull Computer snapshot, int userId) {
@@ -5259,8 +5267,9 @@
                 if (!snapshot.isPackageSuspendedForUser(packageName, userId)) {
                     return null;
                 }
-                return mSuspendPackageHelper.getSuspendingPackage(snapshot, packageName, userId,
-                        callingUid);
+                final UserPackage suspender = mSuspendPackageHelper.getSuspendingPackage(
+                        snapshot, packageName, userId, callingUid);
+                return suspender != null ? suspender.packageName : null;
             } catch (PackageManager.NameNotFoundException e) {
                 return null;
             }
@@ -6198,7 +6207,8 @@
         @Override
         public String[] setPackagesSuspendedAsUser(String[] packageNames, boolean suspended,
                 PersistableBundle appExtras, PersistableBundle launcherExtras,
-                SuspendDialogInfo dialogInfo, int flags, String callingPackage, int userId) {
+                SuspendDialogInfo dialogInfo, int flags, String suspendingPackage,
+                int suspendingUserId, int targetUserId) {
             final int callingUid = Binder.getCallingUid();
             boolean quarantined = false;
             if (Flags.quarantinedEnabled()) {
@@ -6207,14 +6217,15 @@
                 } else if (FeatureFlagUtils.isEnabled(mContext,
                         SETTINGS_TREAT_PAUSE_AS_QUARANTINE)) {
                     final String wellbeingPkg = mContext.getString(R.string.config_systemWellbeing);
-                    quarantined = callingPackage.equals(wellbeingPkg);
+                    quarantined = suspendingPackage.equals(wellbeingPkg);
                 }
             }
             final Computer snapshot = snapshotComputer();
-            enforceCanSetPackagesSuspendedAsUser(snapshot, quarantined, callingPackage, callingUid,
-                    userId, "setPackagesSuspendedAsUser");
+            final UserPackage suspender = UserPackage.of(targetUserId, suspendingPackage);
+            enforceCanSetPackagesSuspendedAsUser(snapshot, quarantined, suspender, callingUid,
+                    targetUserId, "setPackagesSuspendedAsUser");
             return mSuspendPackageHelper.setPackagesSuspended(snapshot, packageNames, suspended,
-                    appExtras, launcherExtras, dialogInfo, callingPackage, userId, callingUid,
+                    appExtras, launcherExtras, dialogInfo, suspender, targetUserId, callingUid,
                     quarantined);
         }
 
@@ -6653,7 +6664,7 @@
             final Computer computer = snapshotComputer();
             final String[] allPackages = computer.getAllAvailablePackageNames();
             mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer, allPackages,
-                    (suspendingPackage) -> !PLATFORM_PACKAGE_NAME.equals(suspendingPackage),
+                    (suspender) -> !PLATFORM_PACKAGE_NAME.equals(suspender.packageName),
                     userId);
         }
 
@@ -6667,8 +6678,13 @@
         @Override
         public String[] setPackagesSuspendedByAdmin(
                 @UserIdInt int userId, @NonNull String[] packageNames, boolean suspended) {
-            return mSuspendPackageHelper.setPackagesSuspendedByAdmin(
-                    snapshotComputer(), userId, packageNames, suspended);
+            final int suspendingUserId = userId;
+            final UserPackage suspender = UserPackage.of(
+                    suspendingUserId, PackageManagerService.PLATFORM_PACKAGE_NAME);
+            return mSuspendPackageHelper.setPackagesSuspended(snapshotComputer(), packageNames,
+                    suspended, null /* appExtras */, null /* launcherExtras */,
+                    null /* dialogInfo */, suspender, userId, Process.SYSTEM_UID,
+                    false /* quarantined */);
         }
 
         @Override
@@ -7086,6 +7102,12 @@
             mPackageMonitorCallbackHelper.notifyPackageMonitorWithIntent(intent, userId,
                     visibilityAllowList, mHandler);
         }
+
+        @Override
+        public boolean isUpgradingFromLowerThan(int sdkVersion) {
+            final boolean isUpgrading = mPriorSdkVersion != -1;
+            return isUpgrading && mPriorSdkVersion < sdkVersion;
+        }
     }
 
     private void setEnabledOverlayPackages(@UserIdInt int userId,
@@ -7974,8 +7996,9 @@
         mRemovePackageHelper.removeCodePath(codePath);
     }
 
-    void cleanUpResources(@Nullable File codeFile, @Nullable String[] instructionSets) {
-        mRemovePackageHelper.cleanUpResources(codeFile, instructionSets);
+    void cleanUpResources(@NonNull String packageName, @NonNull File codeFile,
+                          @NonNull String[] instructionSets) {
+        mRemovePackageHelper.cleanUpResources(packageName, codeFile, instructionSets);
     }
 
     void cleanUpForMoveInstall(String volumeUuid, String packageName, String fromCodePath) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
index ebf1c04..049737d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
@@ -27,12 +27,12 @@
 import android.util.DisplayMetrics;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.server.SystemConfig;
 import com.android.server.compat.PlatformCompat;
 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.parsing.PackageParser2;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.resolution.ComponentResolver;
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
index 655b9c9..2d79718 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
@@ -31,10 +31,10 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.om.OverlayConfig;
+import com.android.internal.pm.parsing.PackageParser2;
 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.parsing.PackageParser2;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
 import com.android.server.pm.pkg.AndroidPackage;
 
@@ -65,6 +65,7 @@
     public ComponentName instantAppResolverSettingsComponent;
     public boolean isPreNmr1Upgrade;
     public boolean isPreQupgrade;
+    public int priorSdkVersion = -1;
     public boolean isUpgrade;
     public LegacyPermissionManagerInternal legacyPermissionManagerInternal;
     public DisplayMetrics Metrics;
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 322557b..5724ee0 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -1085,8 +1085,14 @@
         // the sdk or package name along with optional additional information based on opt.
         final Map<String, List<String>> out = new HashMap<>();
         for (int userId : userIds) {
-            final int translatedUserId =
+            final int translatedUserId;
+            try {
+                translatedUserId =
                     translateUserId(userId, UserHandle.USER_SYSTEM, "runListPackages");
+            } catch (RuntimeException ex) {
+                getErrPrintWriter().println("Error: " + ex.toString());
+                continue;
+            }
             @SuppressWarnings("unchecked") final ParceledListSlice<PackageInfo> slice =
                     mInterface.getInstalledPackages(getFlags, translatedUserId);
             final List<PackageInfo> packages = slice.getList();
@@ -2827,7 +2833,7 @@
             mInterface.setPackagesSuspendedAsUser(packageNames.toArray(new String[] {}),
                     suspendedState, ((appExtras.size() > 0) ? appExtras : null),
                     ((launcherExtras.size() > 0) ? launcherExtras : null),
-                    info, flags, callingPackage, translatedUserId);
+                    info, flags, callingPackage, UserHandle.USER_SYSTEM, translatedUserId);
             for (int i = 0; i < packageNames.size(); i++) {
                 final String packageName = packageNames.get(i);
                 pw.println("Package " + packageName + " new suspended state: "
diff --git a/services/core/java/com/android/server/pm/PackageRemovedInfo.java b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
index 881b0b3..6f57538 100644
--- a/services/core/java/com/android/server/pm/PackageRemovedInfo.java
+++ b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
@@ -43,7 +43,7 @@
     // of package changes
     SparseArray<int[]> mBroadcastAllowList;
     // Clean up resources deleted packages.
-    InstallArgs mArgs = null;
+    CleanUpArgs mArgs = null;
     private static final int[] EMPTY_INT_ARRAY = new int[0];
 
     public void populateBroadcastUsers(PackageSetting deletedPackageSetting) {
diff --git a/services/core/java/com/android/server/pm/PackageSessionVerifier.java b/services/core/java/com/android/server/pm/PackageSessionVerifier.java
index 15d2fdc..1fe49c7 100644
--- a/services/core/java/com/android/server/pm/PackageSessionVerifier.java
+++ b/services/core/java/com/android/server/pm/PackageSessionVerifier.java
@@ -41,10 +41,11 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.InstallLocationUtils;
+import com.android.internal.pm.parsing.PackageParser2;
+import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.rollback.RollbackManagerInternal;
 
 import java.io.File;
@@ -399,7 +400,7 @@
             try (PackageParser2 packageParser = mPackageParserSupplier.get()) {
                 File apexFile = new File(apexInfo.modulePath);
                 parsedPackage = packageParser.parsePackage(apexFile, 0, false);
-            } catch (PackageManagerException e) {
+            } catch (PackageParserException e) {
                 throw new PackageManagerException(
                         PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                         "Failed to parse APEX package " + apexInfo.modulePath + " : " + e, e);
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 7d0a1f6..02ce159 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -32,6 +32,7 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.SigningInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.os.UserHandle;
 import android.os.incremental.IncrementalManager;
@@ -71,6 +72,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -172,6 +174,8 @@
     private File mPath;
     @NonNull
     private String mPathString;
+    @Nullable
+    private LinkedHashSet<File> mOldPaths;
 
     private float mLoadingProgress;
     private long mLoadingCompletedTime;
@@ -682,6 +686,7 @@
         pkg = other.pkg;
         mPath = other.mPath;
         mPathString = other.mPathString;
+        mOldPaths = other.mOldPaths == null ? null : new LinkedHashSet<>(other.mOldPaths);
         mPrimaryCpuAbi = other.mPrimaryCpuAbi;
         mSecondaryCpuAbi = other.mSecondaryCpuAbi;
         mCpuAbiOverride = other.mCpuAbiOverride;
@@ -952,7 +957,7 @@
 
     void setUserState(int userId, long ceDataInode, long deDataInode, int enabled,
                       boolean installed, boolean stopped, boolean notLaunched, boolean hidden,
-                      int distractionFlags, ArrayMap<String, SuspendParams> suspendParams,
+                      int distractionFlags, ArrayMap<UserPackage, SuspendParams> suspendParams,
                       boolean instantApp, boolean virtualPreload, String lastDisableAppCaller,
                       ArraySet<String> enabledComponents, ArraySet<String> disabledComponents,
                       int installReason, int uninstallReason,
@@ -1182,7 +1187,7 @@
             if (state.isSuspended()) {
                 for (int j = 0; j < state.getSuspendParams().size(); j++) {
                     proto.write(PackageProto.UserInfoProto.SUSPENDING_PACKAGE,
-                            state.getSuspendParams().keyAt(j));
+                            state.getSuspendParams().keyAt(j).packageName);
                 }
             }
             proto.write(PackageProto.UserInfoProto.IS_STOPPED, state.isStopped());
@@ -1237,6 +1242,27 @@
         return this;
     }
 
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public PackageSetting addOldPath(@NonNull File path) {
+        if (mOldPaths == null) {
+            mOldPaths = new LinkedHashSet<>();
+        }
+        mOldPaths.add(path);
+        onChanged();
+        return this;
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public PackageSetting removeOldPath(@Nullable File path) {
+        if (path == null || mOldPaths == null) {
+            return this;
+        }
+        if (mOldPaths.remove(path)) {
+            onChanged();
+        }
+        return this;
+    }
+
     /**
      * @param userId the specific user to change the label/icon for
      * @see PackageUserStateImpl#overrideLabelAndIcon(ComponentName, String, Integer)
@@ -1667,6 +1693,11 @@
     }
 
     @DataClass.Generated.Member
+    public @Nullable LinkedHashSet<File> getOldPaths() {
+        return mOldPaths;
+    }
+
+    @DataClass.Generated.Member
     public float getLoadingProgress() {
         return mLoadingProgress;
     }
@@ -1748,10 +1779,10 @@
     }
 
     @DataClass.Generated(
-            time = 1700251133016L,
+            time = 1702666890740L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java",
-            inputSignatures = "private  int mBooleans\nprivate  int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate  int mAppId\nprivate @android.annotation.Nullable com.android.internal.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate  float mLoadingProgress\nprivate  long mLoadingCompletedTime\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate  long mLastModifiedTime\nprivate  long lastUpdateTime\nprivate  long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate  int categoryOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate @android.annotation.Nullable java.lang.String mAppMetadataFilePath\nprivate  int mTargetSdkVersion\nprivate @android.annotation.Nullable byte[] mRestrictUpdateHash\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic  com.android.server.pm.PackageSetting snapshot()\npublic  void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic  com.android.server.pm.PackageSetting setAppId(int)\npublic  com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic  com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic  com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic  com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic  com.android.server.pm.PackageSetting setInstallerPackage(java.lang.String,int)\npublic  com.android.server.pm.PackageSetting setUpdateOwnerPackage(java.lang.String)\npublic  com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n  com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic  com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic  com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic  com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic  com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic  com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic  boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic  com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic  com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic  com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic  com.android.server.pm.PackageSetting setSharedUserAppId(int)\npublic  com.android.server.pm.PackageSetting setTargetSdkVersion(int)\npublic  com.android.server.pm.PackageSetting setRestrictUpdateHash(byte[])\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprivate  void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  void updateFrom(com.android.server.pm.PackageSetting)\n  com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic  com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic  boolean isPrivileged()\npublic  boolean isOem()\npublic  boolean isVendor()\npublic  boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic  boolean isSystemExt()\npublic  boolean isOdm()\npublic  boolean isSystem()\npublic  boolean isRequestLegacyExternalStorage()\npublic  boolean isUserDataFragile()\npublic  android.content.pm.SigningDetails getSigningDetails()\npublic  com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic  void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic  com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n  void setEnabled(int,int,java.lang.String)\n  int getEnabled(int)\n  void setInstalled(boolean,int)\n  boolean getInstalled(int)\n  int getInstallReason(int)\n  void setInstallReason(int,int)\n  int getUninstallReason(int)\n  void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n  boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n  boolean isInstalledOrHasDataOnAnyOtherUser(int[],int)\n  int[] queryInstalledUsers(int[],boolean)\n  int[] queryUsersInstalledOrHasData(int[])\n  long getCeDataInode(int)\n  long getDeDataInode(int)\n  void setCeDataInode(long,int)\n  void setDeDataInode(long,int)\n  boolean getStopped(int)\n  void setStopped(boolean,int)\npublic  com.android.server.pm.PackageSetting setScannedAsStoppedSystemApp(boolean)\n  boolean getNotLaunched(int)\n  void setNotLaunched(boolean,int)\n  boolean getHidden(int)\n  void setHidden(boolean,int)\n  int getDistractionFlags(int)\n  void setDistractionFlags(int,int)\npublic  boolean getInstantApp(int)\n  void setInstantApp(boolean,int)\n  boolean getVirtualPreload(int)\n  void setVirtualPreload(boolean,int)\n  void setUserState(int,long,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long,int,com.android.server.pm.pkg.ArchiveState)\n  void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n  void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n  void addDisabledComponent(java.lang.String,int)\n  void addEnabledComponent(java.lang.String,int)\n  boolean enableComponentLPw(java.lang.String,int)\n  boolean disableComponentLPw(java.lang.String,int)\n  boolean restoreComponentLPw(java.lang.String,int)\n  int getCurrentEnabledStateLPr(java.lang.String,int)\n  void removeUser(int)\npublic  int[] getNotInstalledUserIds()\n  void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected  void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\nprivate static  void writeArchiveState(android.util.proto.ProtoOutputStream,com.android.server.pm.pkg.ArchiveState)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic  void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic  boolean isIncremental()\npublic  boolean isLoading()\npublic  com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic  com.android.server.pm.PackageSetting setLoadingCompletedTime(long)\npublic  com.android.server.pm.PackageSetting setAppMetadataFilePath(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getSharedLibraryDependencies()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getApexModuleName()\npublic  com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic  com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic  com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic  com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic  com.android.server.pm.PackageSetting setApexModuleName(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic  com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbi()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbi()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getSeInfo()\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbiLegacy()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbiLegacy()\npublic @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy @java.lang.Override int getHiddenApiEnforcementPolicy()\npublic @java.lang.Override boolean isApex()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isDefaultToDeviceProtectedStorage()\npublic @java.lang.Override boolean isPersistent()\npublic @java.lang.Override boolean isScannedAsStoppedSystemApp()\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\nprivate static final  int INSTALL_PERMISSION_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int SCANNED_AS_STOPPED_SYSTEM_APP\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
+            inputSignatures = "private  int mBooleans\nprivate  int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable boolean[] usesSdkLibrariesOptional\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate  int mAppId\nprivate @android.annotation.Nullable com.android.internal.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate @android.annotation.Nullable java.util.LinkedHashSet<java.io.File> mOldPaths\nprivate  float mLoadingProgress\nprivate  long mLoadingCompletedTime\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate  long mLastModifiedTime\nprivate  long lastUpdateTime\nprivate  long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate  int categoryOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate @android.annotation.Nullable java.lang.String mAppMetadataFilePath\nprivate  int mTargetSdkVersion\nprivate @android.annotation.Nullable byte[] mRestrictUpdateHash\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic  com.android.server.pm.PackageSetting snapshot()\npublic  void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic  com.android.server.pm.PackageSetting setAppId(int)\npublic  com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic  com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic  com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic  com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic  com.android.server.pm.PackageSetting setInstallerPackage(java.lang.String,int)\npublic  com.android.server.pm.PackageSetting setUpdateOwnerPackage(java.lang.String)\npublic  com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n  com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic  com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic  com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic  com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic  com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic  com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic  boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic  com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic  com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic  com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic  com.android.server.pm.PackageSetting setSharedUserAppId(int)\npublic  com.android.server.pm.PackageSetting setTargetSdkVersion(int)\npublic  com.android.server.pm.PackageSetting setRestrictUpdateHash(byte[])\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprivate  void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  void updateFrom(com.android.server.pm.PackageSetting)\n  com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic  com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic  boolean isPrivileged()\npublic  boolean isOem()\npublic  boolean isVendor()\npublic  boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic  boolean isSystemExt()\npublic  boolean isOdm()\npublic  boolean isSystem()\npublic  boolean isRequestLegacyExternalStorage()\npublic  boolean isUserDataFragile()\npublic  android.content.pm.SigningDetails getSigningDetails()\npublic  com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic  void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic  com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n  void setEnabled(int,int,java.lang.String)\n  int getEnabled(int)\n  void setInstalled(boolean,int)\n  void setArchiveState(com.android.server.pm.pkg.ArchiveState,int)\n  boolean getInstalled(int)\n  boolean isArchived(int)\n  int getInstallReason(int)\n  void setInstallReason(int,int)\n  int getUninstallReason(int)\n  void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n  boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n  boolean isInstalledOrHasDataOnAnyOtherUser(int[],int)\n  int[] queryInstalledUsers(int[],boolean)\n  int[] queryUsersInstalledOrHasData(int[])\n  long getCeDataInode(int)\n  long getDeDataInode(int)\n  void setCeDataInode(long,int)\n  void setDeDataInode(long,int)\n  boolean getStopped(int)\n  void setStopped(boolean,int)\npublic  com.android.server.pm.PackageSetting setScannedAsStoppedSystemApp(boolean)\n  boolean getNotLaunched(int)\n  void setNotLaunched(boolean,int)\n  boolean getHidden(int)\n  void setHidden(boolean,int)\n  int getDistractionFlags(int)\n  void setDistractionFlags(int,int)\npublic  boolean getInstantApp(int)\n  void setInstantApp(boolean,int)\n  boolean getVirtualPreload(int)\n  void setVirtualPreload(boolean,int)\n  void setUserState(int,long,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long,int,com.android.server.pm.pkg.ArchiveState)\n  void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n  void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n  void addDisabledComponent(java.lang.String,int)\n  void addEnabledComponent(java.lang.String,int)\n  boolean enableComponentLPw(java.lang.String,int)\n  boolean disableComponentLPw(java.lang.String,int)\n  boolean restoreComponentLPw(java.lang.String,int)\n  int getCurrentEnabledStateLPr(java.lang.String,int)\n  void removeUser(int)\npublic  int[] getNotInstalledUserIds()\n  void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected  void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\nprivate static  void writeArchiveState(android.util.proto.ProtoOutputStream,com.android.server.pm.pkg.ArchiveState)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting addOldPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting removeOldPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic  void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic  boolean isIncremental()\npublic  boolean isLoading()\npublic  com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic  com.android.server.pm.PackageSetting setLoadingCompletedTime(long)\npublic  com.android.server.pm.PackageSetting setAppMetadataFilePath(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override boolean[] getUsesSdkLibrariesOptional()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getSharedLibraryDependencies()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getApexModuleName()\npublic  com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic  com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic  com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic  com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic  com.android.server.pm.PackageSetting setUsesSdkLibrariesOptional(boolean[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic  com.android.server.pm.PackageSetting setApexModuleName(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic  com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbi()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbi()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getSeInfo()\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbiLegacy()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbiLegacy()\npublic @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy @java.lang.Override int getHiddenApiEnforcementPolicy()\npublic @java.lang.Override boolean isApex()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isDefaultToDeviceProtectedStorage()\npublic @java.lang.Override boolean isPersistent()\npublic @java.lang.Override boolean isScannedAsStoppedSystemApp()\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\nprivate static final  int INSTALL_PERMISSION_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int SCANNED_AS_STOPPED_SYSTEM_APP\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/ParallelPackageParser.java b/services/core/java/com/android/server/pm/ParallelPackageParser.java
index 1089ac9..0511639 100644
--- a/services/core/java/com/android/server/pm/ParallelPackageParser.java
+++ b/services/core/java/com/android/server/pm/ParallelPackageParser.java
@@ -22,9 +22,10 @@
 import android.os.Trace;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.pm.parsing.PackageParser2;
+import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.util.ConcurrentUtils;
-import com.android.server.pm.parsing.PackageParser2;
 
 import java.io.File;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -125,6 +126,10 @@
     @VisibleForTesting
     protected ParsedPackage parsePackage(File scanFile, int parseFlags)
             throws PackageManagerException {
-        return mPackageParser.parsePackage(scanFile, parseFlags, true);
+        try {
+            return mPackageParser.parsePackage(scanFile, parseFlags, true);
+        } catch (PackageParserException e) {
+            throw new PackageManagerException(e.error, e.getMessage(), e);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index f42b43f..2854453 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -30,6 +30,7 @@
 import static com.android.server.pm.PackageManagerService.TAG;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 import android.content.pm.parsing.ApkLiteParseUtils;
 import android.content.pm.parsing.PackageLite;
@@ -456,15 +457,25 @@
         }
     }
 
-    void cleanUpResources(File codeFile, String[] instructionSets) {
+    void cleanUpResources(@Nullable String packageName, @Nullable File codeFile,
+                          @Nullable String[] instructionSets) {
         synchronized (mPm.mInstallLock) {
             cleanUpResourcesLI(codeFile, instructionSets);
         }
+        if (packageName == null) {
+            return;
+        }
+        synchronized (mPm.mLock) {
+            PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
+            if (ps != null) {
+                ps.removeOldPath(codeFile);
+            }
+        }
     }
 
     // Need installer lock especially for dex file removal.
     @GuardedBy("mPm.mInstallLock")
-    private void cleanUpResourcesLI(File codeFile, String[] instructionSets) {
+    private void cleanUpResourcesLI(@Nullable File codeFile, @Nullable String[] instructionSets) {
         // Try enumerating all code paths before deleting
         List<String> allCodePaths = Collections.EMPTY_LIST;
         if (codeFile != null && codeFile.exists()) {
@@ -482,7 +493,8 @@
     }
 
     @GuardedBy("mPm.mInstallLock")
-    private void removeDexFilesLI(List<String> allCodePaths, String[] instructionSets) {
+    private void removeDexFilesLI(@NonNull List<String> allCodePaths,
+                                  @Nullable  String[] instructionSets) {
         if (!allCodePaths.isEmpty()) {
             if (instructionSets == null) {
                 throw new IllegalStateException("instructionSet == null");
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 5c6d61e..2e67b2f 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -37,6 +37,7 @@
 import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_VIRTUAL_PRELOAD;
 import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
+import static com.android.server.pm.PackageManagerService.SCAN_DONT_KILL_APP;
 import static com.android.server.pm.PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE;
 import static com.android.server.pm.PackageManagerService.SCAN_MOVE;
 import static com.android.server.pm.PackageManagerService.SCAN_NEW_INSTALL;
@@ -227,6 +228,7 @@
             // make a deep copy to avoid modifying any existing system state.
             pkgSetting = new PackageSetting(pkgSetting);
             pkgSetting.setPkg(parsedPackage);
+            final boolean isDontKill = (scanFlags & SCAN_DONT_KILL_APP) != 0;
 
             // REMOVE SharedUserSetting from method; update in a separate call.
             //
@@ -244,7 +246,8 @@
                     parsedPackage.getUsesSdkLibrariesOptional(),
                     usesStaticLibraries, parsedPackage.getUsesStaticLibrariesVersions(),
                     parsedPackage.getMimeGroups(), newDomainSetId,
-                    parsedPackage.getTargetSdkVersion(), parsedPackage.getRestrictUpdateHash());
+                    parsedPackage.getTargetSdkVersion(), parsedPackage.getRestrictUpdateHash(),
+                    isDontKill);
         }
 
         if (createNewPackage && originalPkgSetting != null) {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 460bbb7..edae273 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -41,6 +41,7 @@
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.Flags;
 import android.content.pm.IntentFilterVerificationInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
@@ -51,6 +52,7 @@
 import android.content.pm.Signature;
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.VerifierDeviceIdentity;
 import android.content.pm.overlay.OverlayPaths;
 import android.net.Uri;
@@ -1227,9 +1229,10 @@
             @Nullable boolean[] usesSdkLibrariesOptional,
             @Nullable String[] usesStaticLibraries, @Nullable long[] usesStaticLibrariesVersions,
             @Nullable Set<String> mimeGroupNames, @NonNull UUID domainSetId,
-            int targetSdkVersion, byte[] restrictUpdatedHash)
+            int targetSdkVersion, byte[] restrictUpdatedHash, boolean isDontKill)
                     throws PackageManagerException {
         final String pkgName = pkgSetting.getPackageName();
+        final File oldCodePath = pkgSetting.getPath();
         if (sharedUser != null) {
             if (!Objects.equals(existingSharedUserSetting, sharedUser)) {
                 PackageManagerService.reportSettingsProblem(Log.WARN,
@@ -1246,7 +1249,7 @@
             pkgSetting.setSharedUserAppId(INVALID_UID);
         }
 
-        if (!pkgSetting.getPath().equals(codePath)) {
+        if (!oldCodePath.equals(codePath)) {
             final boolean isSystem = pkgSetting.isSystem();
             Slog.i(PackageManagerService.TAG,
                     "Update" + (isSystem ? " system" : "")
@@ -1274,6 +1277,11 @@
                 pkgSetting.setLegacyNativeLibraryPath(legacyNativeLibraryPath);
             }
             pkgSetting.setPath(codePath);
+            if (isDontKill && Flags.improveInstallDontKill()) {
+                // We retain old code paths for DONT_KILL installs. Keep a record of old paths until
+                // they are removed.
+                pkgSetting.addOldPath(oldCodePath);
+            }
         }
 
         pkgSetting.setPrimaryCpuAbi(primaryCpuAbi)
@@ -1956,7 +1964,7 @@
                         ArchiveState archiveState = null;
 
                         int packageDepth = parser.getDepth();
-                        ArrayMap<String, SuspendParams> suspendParamsMap = null;
+                        ArrayMap<UserPackage, SuspendParams> suspendParamsMap = null;
                         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                                 && (type != XmlPullParser.END_TAG
                                 || parser.getDepth() > packageDepth)) {
@@ -1983,18 +1991,15 @@
                                             parser);
                                     break;
                                 case TAG_SUSPEND_PARAMS:
-                                    final String suspendingPackage = parser.getAttributeValue(null,
-                                            ATTR_SUSPENDING_PACKAGE);
-                                    if (suspendingPackage == null) {
-                                        Slog.wtf(TAG, "No suspendingPackage found inside tag "
-                                                + TAG_SUSPEND_PARAMS);
+                                    Map.Entry<UserPackage, SuspendParams> entry =
+                                            readSuspensionParamsLPr(userId, parser);
+                                    if (entry == null) {
                                         continue;
                                     }
                                     if (suspendParamsMap == null) {
                                         suspendParamsMap = new ArrayMap<>();
                                     }
-                                    suspendParamsMap.put(suspendingPackage,
-                                            SuspendParams.restoreFromXml(parser));
+                                    suspendParamsMap.put(entry.getKey(), entry.getValue());
                                     break;
                                 case TAG_ARCHIVE_STATE:
                                     archiveState = parseArchiveState(parser);
@@ -2016,7 +2021,8 @@
                                     oldSuspendedLauncherExtras,
                                     false /* quarantined */);
                             suspendParamsMap = new ArrayMap<>();
-                            suspendParamsMap.put(oldSuspendingPackage, suspendParams);
+                            suspendParamsMap.put(
+                                    UserPackage.of(userId, oldSuspendingPackage), suspendParams);
                         }
 
                         if (blockUninstall) {
@@ -2058,6 +2064,20 @@
         }
     }
 
+    @Nullable
+    private static Map.Entry<UserPackage, SuspendParams> readSuspensionParamsLPr(
+            int userId, TypedXmlPullParser parser) throws IOException {
+        final String suspendingPackage = parser.getAttributeValue(null, ATTR_SUSPENDING_PACKAGE);
+        if (suspendingPackage == null) {
+            Slog.wtf(TAG, "No suspendingPackage found inside tag " + TAG_SUSPEND_PARAMS);
+            return null;
+        }
+        final int suspendingUserId = userId;
+        return Map.entry(
+                UserPackage.of(suspendingUserId, suspendingPackage),
+                SuspendParams.restoreFromXml(parser));
+    }
+
     private static ArchiveState parseArchiveState(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
         String installerTitle = parser.getAttributeValue(null,
@@ -2414,10 +2434,11 @@
                         }
                         if (ustate.isSuspended()) {
                             for (int i = 0; i < ustate.getSuspendParams().size(); i++) {
-                                final String suspendingPackage = ustate.getSuspendParams().keyAt(i);
+                                final UserPackage suspendingPackage =
+                                        ustate.getSuspendParams().keyAt(i);
                                 serializer.startTag(null, TAG_SUSPEND_PARAMS);
                                 serializer.attribute(null, ATTR_SUSPENDING_PACKAGE,
-                                        suspendingPackage);
+                                        suspendingPackage.packageName);
                                 final SuspendParams params =
                                         ustate.getSuspendParams().valueAt(i);
                                 if (params != null) {
@@ -4955,6 +4976,11 @@
         }
         pw.print(prefix); pw.print("  pkg="); pw.println(pkg);
         pw.print(prefix); pw.print("  codePath="); pw.println(ps.getPathString());
+        if (ps.getOldPaths() != null && ps.getOldPaths().size() > 0) {
+            for (File oldPath : ps.getOldPaths()) {
+                pw.print(prefix); pw.println("    oldCodePath=" + oldPath.getAbsolutePath());
+            }
+        }
         if (permissionNames == null) {
             pw.print(prefix); pw.print("  resourcePath="); pw.println(ps.getPathString());
             pw.print(prefix); pw.print("  legacyNativeLibraryDir=");
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index c2a960a..4e70cc5 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -27,10 +27,10 @@
 import android.app.AppOpsManager;
 import android.content.Intent;
 import android.content.pm.SuspendDialogInfo;
+import android.content.pm.UserPackage;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.PersistableBundle;
-import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
@@ -88,8 +88,8 @@
      * @param dialogInfo An optional {@link SuspendDialogInfo} object describing the dialog that
      *                   should be shown to the user when they try to launch a suspended app.
      *                   Ignored if {@code suspended} is false.
-     * @param callingPackage The caller's package name.
-     * @param userId The user where packages reside.
+     * @param suspendingPackage The caller's package name.
+     * @param targetUserId The user where packages reside.
      * @param callingUid The caller's uid.
      * @return The names of failed packages.
      */
@@ -97,14 +97,14 @@
     String[] setPackagesSuspended(@NonNull Computer snapshot, @Nullable String[] packageNames,
             boolean suspended, @Nullable PersistableBundle appExtras,
             @Nullable PersistableBundle launcherExtras, @Nullable SuspendDialogInfo dialogInfo,
-            @NonNull String callingPackage, @UserIdInt int userId, int callingUid,
+            @NonNull UserPackage suspendingPackage, @UserIdInt int targetUserId, int callingUid,
             boolean quarantined) {
         if (ArrayUtils.isEmpty(packageNames)) {
             return packageNames;
         }
-        if (suspended && !quarantined && !isSuspendAllowedForUser(snapshot, userId,
-                callingUid)) {
-            Slog.w(TAG, "Cannot suspend due to restrictions on user " + userId);
+        if (suspended && !quarantined
+                && !isSuspendAllowedForUser(snapshot, targetUserId, callingUid)) {
+            Slog.w(TAG, "Cannot suspend due to restrictions on user " + targetUserId);
             return packageNames;
         }
 
@@ -119,19 +119,21 @@
         final IntArray changedUids = new IntArray(packageNames.length);
 
         final boolean[] canSuspend = suspended
-                ? canSuspendPackageForUser(snapshot, packageNames, userId, callingUid)
+                ? canSuspendPackageForUser(snapshot, packageNames, targetUserId, callingUid)
                 : null;
         for (int i = 0; i < packageNames.length; i++) {
             final String packageName = packageNames[i];
-            if (callingPackage.equals(packageName)) {
-                Slog.w(TAG, "Calling package: " + callingPackage + " trying to "
+            if (suspendingPackage.packageName.equals(packageName)
+                    && suspendingPackage.userId == targetUserId) {
+                Slog.w(TAG, "Suspending package: " + suspendingPackage + " trying to "
                         + (suspended ? "" : "un") + "suspend itself. Ignoring");
                 unmodifiablePackages.add(packageName);
                 continue;
             }
             final PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName);
-            if (packageState == null || !packageState.getUserStateOrDefault(userId).isInstalled()
-                    || snapshot.shouldFilterApplication(packageState, callingUid, userId)) {
+            if (packageState == null
+                    || !packageState.getUserStateOrDefault(targetUserId).isInstalled()
+                    || snapshot.shouldFilterApplication(packageState, callingUid, targetUserId)) {
                 Slog.w(TAG, "Could not find package setting for package: " + packageName
                         + ". Skipping suspending/un-suspending.");
                 unmodifiablePackages.add(packageName);
@@ -142,34 +144,34 @@
                 continue;
             }
 
-            final WatchedArrayMap<String, SuspendParams> suspendParamsMap =
-                    packageState.getUserStateOrDefault(userId).getSuspendParams();
+            final WatchedArrayMap<UserPackage, SuspendParams> suspendParamsMap =
+                    packageState.getUserStateOrDefault(targetUserId).getSuspendParams();
             final SuspendParams oldSuspendParams = suspendParamsMap == null
-                    ? null : suspendParamsMap.get(callingPackage);
+                    ? null : suspendParamsMap.get(suspendingPackage);
             boolean changed = !Objects.equals(oldSuspendParams, newSuspendParams);
 
             if (suspended && !changed) {
                 // Carried over API behavior, must notify change even if no change
                 notifyPackagesList.add(packageName);
                 notifyUids.add(
-                        UserHandle.getUid(userId, packageState.getAppId()));
+                        UserHandle.getUid(targetUserId, packageState.getAppId()));
                 continue;
             }
 
-            // If only the callingPackage is suspending this package,
+            // If only the suspendingPackage is suspending this package,
             // it will be unsuspended when this change is committed
             boolean packageUnsuspended = !suspended
                     && CollectionUtils.size(suspendParamsMap) == 1
-                    && suspendParamsMap.containsKey(callingPackage);
+                    && suspendParamsMap.containsKey(suspendingPackage);
             if (suspended || packageUnsuspended) {
                 // Always notify of a suspend call + notify when fully unsuspended
                 notifyPackagesList.add(packageName);
-                notifyUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+                notifyUids.add(UserHandle.getUid(targetUserId, packageState.getAppId()));
             }
 
             if (changed) {
                 changedPackagesList.add(packageName);
-                changedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+                changedUids.add(UserHandle.getUid(targetUserId, packageState.getAppId()));
             } else {
                 Slog.w(TAG, "No change is needed for package: " + packageName
                         + ". Skipping suspending/un-suspending.");
@@ -181,11 +183,11 @@
             for (int index = 0; index < size; index++) {
                 final String packageName  = changedPackagesList.valueAt(index);
                 final PackageUserStateWrite userState = mutator.forPackage(packageName)
-                        .userState(userId);
+                        .userState(targetUserId);
                 if (suspended) {
-                    userState.putSuspendParams(callingPackage, newSuspendParams);
+                    userState.putSuspendParams(suspendingPackage, newSuspendParams);
                 } else {
-                    userState.removeSuspension(callingPackage);
+                    userState.removeSuspension(suspendingPackage);
                 }
             }
         });
@@ -197,17 +199,17 @@
             mBroadcastHelper.sendPackagesSuspendedOrUnsuspendedForUser(newSnapshot,
                     suspended ? Intent.ACTION_PACKAGES_SUSPENDED
                             : Intent.ACTION_PACKAGES_UNSUSPENDED,
-                    changedPackages, notifyUids.toArray(), quarantined, userId);
+                    changedPackages, notifyUids.toArray(), quarantined, targetUserId);
             mBroadcastHelper.sendMyPackageSuspendedOrUnsuspended(newSnapshot, changedPackages,
-                    suspended, userId);
-            mPm.scheduleWritePackageRestrictions(userId);
+                    suspended, targetUserId);
+            mPm.scheduleWritePackageRestrictions(targetUserId);
         }
         // Send the suspension changed broadcast to ensure suspension state is not stale.
         if (!changedPackagesList.isEmpty()) {
             mBroadcastHelper.sendPackagesSuspendedOrUnsuspendedForUser(newSnapshot,
                     Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
                     changedPackagesList.toArray(new String[0]), changedUids.toArray(), quarantined,
-                    userId);
+                    targetUserId);
         }
         return unmodifiablePackages.toArray(new String[0]);
     }
@@ -216,19 +218,19 @@
      * Returns the names in the {@code packageNames} which can not be suspended by the caller.
      *
      * @param packageNames The names of packages to check.
-     * @param userId The user where packages reside.
+     * @param targetUserId The user where packages reside.
      * @param callingUid The caller's uid.
      * @return The names of packages which are Unsuspendable.
      */
     @NonNull
     String[] getUnsuspendablePackagesForUser(@NonNull Computer snapshot,
-            @NonNull String[] packageNames, @UserIdInt int userId, int callingUid) {
-        if (!isSuspendAllowedForUser(snapshot, userId, callingUid)) {
-            Slog.w(TAG, "Cannot suspend due to restrictions on user " + userId);
+            @NonNull String[] packageNames, @UserIdInt int targetUserId, int callingUid) {
+        if (!isSuspendAllowedForUser(snapshot, targetUserId, callingUid)) {
+            Slog.w(TAG, "Cannot suspend due to restrictions on user " + targetUserId);
             return packageNames;
         }
         final ArraySet<String> unactionablePackages = new ArraySet<>();
-        final boolean[] canSuspend = canSuspendPackageForUser(snapshot, packageNames, userId,
+        final boolean[] canSuspend = canSuspendPackageForUser(snapshot, packageNames, targetUserId,
                 callingUid);
         for (int i = 0; i < packageNames.length; i++) {
             if (!canSuspend[i]) {
@@ -237,7 +239,7 @@
             }
             final PackageStateInternal packageState =
                     snapshot.getPackageStateForInstalledAndFiltered(
-                            packageNames[i], callingUid, userId);
+                            packageNames[i], callingUid, targetUserId);
             if (packageState == null) {
                 Slog.w(TAG, "Could not find package setting for package: " + packageNames[i]);
                 unactionablePackages.add(packageNames[i]);
@@ -285,30 +287,31 @@
      * @param packagesToChange The packages on which the suspension are to be removed.
      * @param suspendingPackagePredicate A predicate identifying the suspending packages whose
      *                                   suspensions will be removed.
-     * @param userId The user for which the changes are taking place.
+     * @param targetUserId The user for which the changes are taking place.
      */
     void removeSuspensionsBySuspendingPackage(@NonNull Computer snapshot,
             @NonNull String[] packagesToChange,
-            @NonNull Predicate<String> suspendingPackagePredicate, int userId) {
+            @NonNull Predicate<UserPackage> suspendingPackagePredicate, int targetUserId) {
         final List<String> unsuspendedPackages = new ArrayList<>();
         final IntArray unsuspendedUids = new IntArray();
-        final ArrayMap<String, ArraySet<String>> pkgToSuspendingPkgsToCommit = new ArrayMap<>();
+        final ArrayMap<String, ArraySet<UserPackage>> pkgToSuspendingPkgsToCommit =
+                new ArrayMap<>();
         for (String packageName : packagesToChange) {
             final PackageStateInternal packageState =
                     snapshot.getPackageStateInternal(packageName);
             final PackageUserStateInternal packageUserState = packageState == null
-                    ? null : packageState.getUserStateOrDefault(userId);
+                    ? null : packageState.getUserStateOrDefault(targetUserId);
             if (packageUserState == null || !packageUserState.isSuspended()) {
                 continue;
             }
 
-            WatchedArrayMap<String, SuspendParams> suspendParamsMap =
+            WatchedArrayMap<UserPackage, SuspendParams> suspendParamsMap =
                     packageUserState.getSuspendParams();
             int countRemoved = 0;
             for (int index = 0; index < suspendParamsMap.size(); index++) {
-                String suspendingPackage = suspendParamsMap.keyAt(index);
+                UserPackage suspendingPackage = suspendParamsMap.keyAt(index);
                 if (suspendingPackagePredicate.test(suspendingPackage)) {
-                    ArraySet<String> suspendingPkgsToCommit =
+                    ArraySet<UserPackage> suspendingPkgsToCommit =
                             pkgToSuspendingPkgsToCommit.get(packageName);
                     if (suspendingPkgsToCommit == null) {
                         suspendingPkgsToCommit = new ArraySet<>();
@@ -322,31 +325,33 @@
             // Everything would be removed and package unsuspended
             if (countRemoved == suspendParamsMap.size()) {
                 unsuspendedPackages.add(packageState.getPackageName());
-                unsuspendedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+                unsuspendedUids.add(UserHandle.getUid(targetUserId, packageState.getAppId()));
             }
         }
 
         mPm.commitPackageStateMutation(null, mutator -> {
             for (int mapIndex = 0; mapIndex < pkgToSuspendingPkgsToCommit.size(); mapIndex++) {
                 String packageName = pkgToSuspendingPkgsToCommit.keyAt(mapIndex);
-                ArraySet<String> packagesToRemove = pkgToSuspendingPkgsToCommit.valueAt(mapIndex);
-                PackageUserStateWrite userState = mutator.forPackage(packageName).userState(userId);
+                ArraySet<UserPackage> packagesToRemove =
+                        pkgToSuspendingPkgsToCommit.valueAt(mapIndex);
+                PackageUserStateWrite userState =
+                        mutator.forPackage(packageName).userState(targetUserId);
                 for (int setIndex = 0; setIndex < packagesToRemove.size(); setIndex++) {
                     userState.removeSuspension(packagesToRemove.valueAt(setIndex));
                 }
             }
         });
 
-        mPm.scheduleWritePackageRestrictions(userId);
+        mPm.scheduleWritePackageRestrictions(targetUserId);
         final Computer newSnapshot = mPm.snapshotComputer();
         if (!unsuspendedPackages.isEmpty()) {
             final String[] packageArray = unsuspendedPackages.toArray(
                     new String[unsuspendedPackages.size()]);
             mBroadcastHelper.sendMyPackageSuspendedOrUnsuspended(newSnapshot, packageArray,
-                    false, userId);
+                    false, targetUserId);
             mBroadcastHelper.sendPackagesSuspendedOrUnsuspendedForUser(newSnapshot,
                     Intent.ACTION_PACKAGES_UNSUSPENDED,
-                    packageArray, unsuspendedUids.toArray(), false, userId);
+                    packageArray, unsuspendedUids.toArray(), false, targetUserId);
         }
     }
 
@@ -404,7 +409,7 @@
      * @return The name of suspending package.
      */
     @Nullable
-    String getSuspendingPackage(@NonNull Computer snapshot, @NonNull String suspendedPackage,
+    UserPackage getSuspendingPackage(@NonNull Computer snapshot, @NonNull String suspendedPackage,
             int userId, int callingUid) {
         final PackageStateInternal packageState = snapshot.getPackageStateInternal(
                 suspendedPackage, callingUid);
@@ -417,13 +422,13 @@
             return null;
         }
 
-        String suspendingPackage = null;
-        String suspendedBySystem = null;
-        String qasPackage = null;
+        UserPackage suspendingPackage = null;
+        UserPackage suspendedBySystem = null;
+        UserPackage qasPackage = null;
         for (int i = 0; i < userState.getSuspendParams().size(); i++) {
             suspendingPackage = userState.getSuspendParams().keyAt(i);
             var suspendParams = userState.getSuspendParams().valueAt(i);
-            if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) {
+            if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage.packageName)) {
                 suspendedBySystem = suspendingPackage;
             }
             if (suspendParams.isQuarantined() && qasPackage == null) {
@@ -451,7 +456,7 @@
      */
     @Nullable
     SuspendDialogInfo getSuspendedDialogInfo(@NonNull Computer snapshot,
-            @NonNull String suspendedPackage, @NonNull String suspendingPackage, int userId,
+            @NonNull String suspendedPackage, @NonNull UserPackage suspendingPackage, int userId,
             int callingUid) {
         final PackageStateInternal packageState = snapshot.getPackageStateInternal(
                 suspendedPackage, callingUid);
@@ -464,7 +469,7 @@
             return null;
         }
 
-        final WatchedArrayMap<String, SuspendParams> suspendParamsMap =
+        final WatchedArrayMap<UserPackage, SuspendParams> suspendParamsMap =
                 userState.getSuspendParams();
         if (suspendParamsMap == null) {
             return null;
@@ -493,34 +498,36 @@
      * be suspended or not.
      *
      * @param packageNames  The package names to check suspendability for.
-     * @param userId The user to check in
+     * @param targetUserId The user to check in
      * @param callingUid The caller's uid.
      * @return An array containing results of the checks
      */
     @NonNull
     boolean[] canSuspendPackageForUser(@NonNull Computer snapshot, @NonNull String[] packageNames,
-            int userId, int callingUid) {
+            int targetUserId, int callingUid) {
         final boolean[] canSuspend = new boolean[packageNames.length];
-        final boolean isCallerOwner = isCallerDeviceOrProfileOwner(snapshot, userId, callingUid);
+        final boolean isCallerOwner =
+                isCallerDeviceOrProfileOwner(snapshot, targetUserId, callingUid);
         final long token = Binder.clearCallingIdentity();
         try {
             final DefaultAppProvider defaultAppProvider = mInjector.getDefaultAppProvider();
-            final String activeLauncherPackageName = defaultAppProvider.getDefaultHome(userId);
-            final String dialerPackageName = defaultAppProvider.getDefaultDialer(userId);
+            final String activeLauncherPackageName =
+                    defaultAppProvider.getDefaultHome(targetUserId);
+            final String dialerPackageName = defaultAppProvider.getDefaultDialer(targetUserId);
             final String requiredInstallerPackage =
-                    getKnownPackageName(snapshot, KnownPackages.PACKAGE_INSTALLER, userId);
+                    getKnownPackageName(snapshot, KnownPackages.PACKAGE_INSTALLER, targetUserId);
             final String requiredUninstallerPackage =
-                    getKnownPackageName(snapshot, KnownPackages.PACKAGE_UNINSTALLER, userId);
+                    getKnownPackageName(snapshot, KnownPackages.PACKAGE_UNINSTALLER, targetUserId);
             final String requiredVerifierPackage =
-                    getKnownPackageName(snapshot, KnownPackages.PACKAGE_VERIFIER, userId);
+                    getKnownPackageName(snapshot, KnownPackages.PACKAGE_VERIFIER, targetUserId);
             final String requiredPermissionControllerPackage =
                     getKnownPackageName(snapshot, KnownPackages.PACKAGE_PERMISSION_CONTROLLER,
-                            userId);
+                            targetUserId);
             for (int i = 0; i < packageNames.length; i++) {
                 canSuspend[i] = false;
                 final String packageName = packageNames[i];
 
-                if (mPm.isPackageDeviceAdmin(packageName, userId)) {
+                if (mPm.isPackageDeviceAdmin(packageName, targetUserId)) {
                     Slog.w(TAG, "Cannot suspend package \"" + packageName
                             + "\": has an active device admin");
                     continue;
@@ -555,12 +562,12 @@
                             + "\": required for permissions management");
                     continue;
                 }
-                if (mProtectedPackages.isPackageStateProtected(userId, packageName)) {
+                if (mProtectedPackages.isPackageStateProtected(targetUserId, packageName)) {
                     Slog.w(TAG, "Cannot suspend package \"" + packageName
                             + "\": protected package");
                     continue;
                 }
-                if (!isCallerOwner && snapshot.getBlockUninstall(userId, packageName)) {
+                if (!isCallerOwner && snapshot.getBlockUninstall(targetUserId, packageName)) {
                     Slog.w(TAG, "Cannot suspend package \"" + packageName
                             + "\": blocked by admin");
                     continue;
@@ -572,7 +579,7 @@
                 PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName);
                 AndroidPackage pkg = packageState == null ? null : packageState.getPkg();
                 if (pkg != null) {
-                    final int uid = UserHandle.getUid(userId, packageState.getAppId());
+                    final int uid = UserHandle.getUid(targetUserId, packageState.getAppId());
                     // Cannot suspend SDK libs as they are controlled by SDK manager.
                     if (pkg.isSdkLibrary()) {
                         Slog.w(TAG, "Cannot suspend package: " + packageName
@@ -614,20 +621,6 @@
                         == AppOpsManager.MODE_ALLOWED;
     }
 
-    /**
-     * Suspends packages on behalf of an admin.
-     *
-     * @return array of packages that are unsuspendable, either because admin is not allowed to
-     * suspend them (e.g. current dialer) or there was other problem (e.g. package not found).
-     */
-    public String[] setPackagesSuspendedByAdmin(
-            Computer snapshot, int userId, String[] packageNames, boolean suspend) {
-        return setPackagesSuspended(snapshot, packageNames, suspend,
-                null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */,
-                PackageManagerService.PLATFORM_PACKAGE_NAME, userId, Process.SYSTEM_UID,
-                false /* quarantined */);
-    }
-
     private String getKnownPackageName(@NonNull Computer snapshot,
             @KnownPackages.KnownPackage int knownPackage, int userId) {
         final String[] knownPackages =
@@ -635,14 +628,15 @@
         return knownPackages.length > 0 ? knownPackages[0] : null;
     }
 
-    private boolean isCallerDeviceOrProfileOwner(@NonNull Computer snapshot, int userId,
+    private boolean isCallerDeviceOrProfileOwner(@NonNull Computer snapshot, int targetUserId,
             int callingUid) {
         if (callingUid == SYSTEM_UID) {
             return true;
         }
-        final String ownerPackage = mProtectedPackages.getDeviceOwnerOrProfileOwnerPackage(userId);
+        final String ownerPackage =
+                mProtectedPackages.getDeviceOwnerOrProfileOwnerPackage(targetUserId);
         if (ownerPackage != null) {
-            return callingUid == snapshot.getPackageUidInternal(ownerPackage, 0, userId,
+            return callingUid == snapshot.getPackageUidInternal(ownerPackage, 0, targetUserId,
                     callingUid);
         }
         return false;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c48eccf..2305d6c 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -7237,7 +7237,8 @@
 
         @Override
         public boolean isUserInitialized(@UserIdInt int userId) {
-            return (getUserInfo(userId).flags & UserInfo.FLAG_INITIALIZED) != 0;
+            final UserInfo userInfo = getUserInfo(userId);
+            return userInfo != null && (userInfo.flags & UserInfo.FLAG_INITIALIZED) != 0;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
index 79c9c8e..b6267c4 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
@@ -28,6 +28,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.pm.parsing.IPackageCacher;
 import com.android.internal.pm.parsing.pkg.PackageImpl;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.ApexManager;
@@ -39,7 +40,7 @@
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicInteger;
 
-public class PackageCacher {
+public class PackageCacher implements IPackageCacher {
 
     private static final String TAG = "PackageCacher";
 
@@ -162,6 +163,7 @@
      * Returns the cached parse result for {@code packageFile} for parse flags {@code flags},
      * or {@code null} if no cached result exists.
      */
+    @Override
     public ParsedPackage getCachedResult(File packageFile, int flags) {
         final String cacheKey = getCacheKey(packageFile, flags);
         final File cacheFile = new File(mCacheDir, cacheKey);
@@ -192,6 +194,7 @@
     /**
      * Caches the parse result for {@code packageFile} with flags {@code flags}.
      */
+    @Override
     public void cacheResult(File packageFile, int flags, ParsedPackage parsed) {
         try {
             final String cacheKey = getCacheKey(packageFile, flags);
diff --git a/services/core/java/com/android/server/pm/parsing/PackageParserUtils.java b/services/core/java/com/android/server/pm/parsing/PackageParserUtils.java
new file mode 100644
index 0000000..03a7a37
--- /dev/null
+++ b/services/core/java/com/android/server/pm/parsing/PackageParserUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.pm.parsing;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.ServiceManager;
+import android.util.Slog;
+
+import com.android.internal.compat.IPlatformCompat;
+import com.android.internal.pm.parsing.PackageParser2;
+import com.android.internal.pm.pkg.parsing.ParsingUtils;
+import com.android.server.SystemConfig;
+import com.android.server.pm.PackageManagerService;
+
+import java.util.Set;
+
+public class PackageParserUtils {
+    /**
+     * For parsing inside the system server but outside of {@link PackageManagerService}.
+     * Generally used for parsing information in an APK that hasn't been installed yet.
+     *
+     * This must be called inside the system process as it relies on {@link ServiceManager}.
+     */
+    @NonNull
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    public static PackageParser2 forParsingFileWithDefaults() {
+        IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(
+                ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+        return new PackageParser2(null /* separateProcesses */, null /* displayMetrics */,
+                null /* cacheDir */, new PackageParser2.Callback() {
+            @Override
+            @SuppressLint("AndroidFrameworkRequiresPermission")
+            public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
+                try {
+                    return platformCompat.isChangeEnabled(changeId, appInfo);
+                } catch (Exception e) {
+                    // This shouldn't happen, but assume enforcement if it does
+                    Slog.wtf(ParsingUtils.TAG, "IPlatformCompat query failed", e);
+                    return true;
+                }
+            }
+
+            @Override
+            public boolean hasFeature(String feature) {
+                // Assume the device doesn't support anything. This will affect permission parsing
+                // and will force <uses-permission/> declarations to include all requiredNotFeature
+                // permissions and exclude all requiredFeature permissions. This mirrors the old
+                // behavior.
+                return false;
+            }
+
+            @Override
+            public Set<String> getHiddenApiWhitelistedApps() {
+                return SystemConfig.getInstance().getHiddenApiWhitelistedApps();
+            }
+
+            @Override
+            public Set<String> getInstallConstraintsAllowlist() {
+                return SystemConfig.getInstance().getInstallConstraintsAllowlist();
+            }
+        });
+    }
+}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 671e031..3afba39 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -3291,7 +3291,7 @@
         final PermissionAllowlist permissionAllowlist =
                 SystemConfig.getInstance().getPermissionAllowlist();
         final String packageName = packageState.getPackageName();
-        if (packageState.isVendor()) {
+        if (packageState.isVendor() || packageState.isOdm()) {
             return permissionAllowlist.getVendorPrivilegedAppAllowlistState(packageName,
                     permissionName);
         } else if (packageState.isProduct()) {
@@ -3386,7 +3386,7 @@
             // the permission's protectionLevel does not have the extra 'vendorPrivileged'
             // flag.
             if (allowed && isPrivilegedPermission && !bp.isVendorPrivileged()
-                    && pkgSetting.isVendor()) {
+                    && (pkgSetting.isVendor() || pkgSetting.isOdm())) {
                 Slog.w(TAG, "Permission " + permissionName
                         + " cannot be granted to privileged vendor apk " + pkg.getPackageName()
                         + " because it isn't a 'vendorPrivileged' permission.");
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
index 2f4ad2d8..15b693c 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.util.ArraySet;
 import android.util.Pair;
@@ -173,7 +174,7 @@
 
     @Nullable
     @Override
-    public WatchedArrayMap<String, SuspendParams> getSuspendParams() {
+    public WatchedArrayMap<UserPackage, SuspendParams> getSuspendParams() {
         return null;
     }
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
index c5ef525..7a5a14d 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -121,7 +122,7 @@
      * Suspending package to suspend params
      */
     @Nullable
-    private WatchedArrayMap<String, SuspendParams> mSuspendParams;
+    private WatchedArrayMap<UserPackage, SuspendParams> mSuspendParams;
 
     @Nullable
     private WatchedArrayMap<ComponentName, Pair<String, Integer>> mComponentLabelIconOverrideMap;
@@ -369,7 +370,10 @@
         return !CollectionUtils.isEmpty(mSuspendParams);
     }
 
-    public PackageUserStateImpl putSuspendParams(@NonNull String suspendingPackage,
+    /**
+     * Adds or updates suspension params by the given package.
+     */
+    public PackageUserStateImpl putSuspendParams(@NonNull UserPackage suspendingPackage,
             @Nullable SuspendParams suspendParams) {
         if (mSuspendParams == null) {
             mSuspendParams = new WatchedArrayMap<>();
@@ -384,7 +388,10 @@
         return this;
     }
 
-    public PackageUserStateImpl removeSuspension(@NonNull String suspendingPackage) {
+    /**
+     * Removes suspension by the given package.
+     */
+    public PackageUserStateImpl removeSuspension(@NonNull UserPackage suspendingPackage) {
         if (mSuspendParams != null) {
             mSuspendParams.remove(suspendingPackage);
             onChanged();
@@ -565,7 +572,7 @@
      * Suspending package to suspend params
      */
     public @NonNull PackageUserStateImpl setSuspendParams(
-            @NonNull ArrayMap<String, SuspendParams> value) {
+            @NonNull ArrayMap<UserPackage, SuspendParams> value) {
         if (value == null) {
             return this;
         }
@@ -778,7 +785,7 @@
      * Suspending package to suspend params
      */
     @DataClass.Generated.Member
-    public @Nullable WatchedArrayMap<String,SuspendParams> getSuspendParams() {
+    public @Nullable WatchedArrayMap<UserPackage,SuspendParams> getSuspendParams() {
         return mSuspendParams;
     }
 
@@ -830,7 +837,7 @@
      * Suspending package to suspend params
      */
     @DataClass.Generated.Member
-    public @NonNull PackageUserStateImpl setSuspendParams(@NonNull WatchedArrayMap<String,SuspendParams> value) {
+    public @NonNull PackageUserStateImpl setSuspendParams(@NonNull WatchedArrayMap<UserPackage,SuspendParams> value) {
         mSuspendParams = value;
         return this;
     }
@@ -909,10 +916,10 @@
     }
 
     @DataClass.Generated(
-            time = 1701470095849L,
+            time = 1701864813354L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java",
-            inputSignatures = "private  int mBooleans\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate  long mCeDataInode\nprivate  long mDeDataInode\nprivate  int mDistractionFlags\nprivate @android.content.pm.PackageManager.EnabledState int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.content.pm.PackageManager.UserMinAspectRatio int mMinAspectRatio\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate @android.annotation.CurrentTimeMillisLong long mFirstInstallTimeMillis\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nprivate @android.annotation.Nullable com.android.server.pm.pkg.ArchiveState mArchiveState\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate  void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic  boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic  void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic  com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic  com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setMinAspectRatio(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTimeMillis(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setArchiveState(com.android.server.pm.pkg.ArchiveState)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate  boolean watchableEquals(com.android.server.utils.Watchable)\nprivate  int watchableHashCode()\nprivate  boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate  int snapshotHashCode()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isQuarantined()\npublic @java.lang.Override boolean dataExists()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\nprivate static final  int INSTALLED\nprivate static final  int STOPPED\nprivate static final  int NOT_LAUNCHED\nprivate static final  int HIDDEN\nprivate static final  int INSTANT_APP\nprivate static final  int VIRTUAL_PRELOADED\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
+            inputSignatures = "private  int mBooleans\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate  long mCeDataInode\nprivate  long mDeDataInode\nprivate  int mDistractionFlags\nprivate @android.content.pm.PackageManager.EnabledState int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.content.pm.PackageManager.UserMinAspectRatio int mMinAspectRatio\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.pm.UserPackage,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate @android.annotation.CurrentTimeMillisLong long mFirstInstallTimeMillis\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nprivate @android.annotation.Nullable com.android.server.pm.pkg.ArchiveState mArchiveState\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate  void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic  boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic  void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic  com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(android.content.pm.UserPackage,com.android.server.pm.pkg.SuspendParams)\npublic  com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(android.content.pm.UserPackage)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setMinAspectRatio(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<android.content.pm.UserPackage,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTimeMillis(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setArchiveState(com.android.server.pm.pkg.ArchiveState)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate  boolean watchableEquals(com.android.server.utils.Watchable)\nprivate  int watchableHashCode()\nprivate  boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate  int snapshotHashCode()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isQuarantined()\npublic @java.lang.Override boolean dataExists()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\nprivate static final  int INSTALLED\nprivate static final  int STOPPED\nprivate static final  int NOT_LAUNCHED\nprivate static final  int HIDDEN\nprivate static final  int INSTANT_APP\nprivate static final  int VIRTUAL_PRELOADED\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateInternal.java
index 46cc830..f8d745c 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateInternal.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateInternal.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
+import android.content.pm.UserPackage;
 import android.content.pm.pkg.FrameworkPackageUserState;
 import android.util.Pair;
 
@@ -38,7 +39,7 @@
 
     // TODO: Make non-null with emptyMap()
     @Nullable
-    WatchedArrayMap<String, SuspendParams> getSuspendParams();
+    WatchedArrayMap<UserPackage, SuspendParams> getSuspendParams();
 
     @Nullable
     WatchedArraySet<String> getDisabledComponentsNoCopy();
diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
index 8430cf7..253eb40 100644
--- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
+++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
@@ -21,6 +21,7 @@
 import android.content.ComponentName;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.util.ArraySet;
 
@@ -349,7 +350,7 @@
 
             @NonNull
             @Override
-            public PackageUserStateWrite putSuspendParams(@NonNull String suspendingPackage,
+            public PackageUserStateWrite putSuspendParams(@NonNull UserPackage suspendingPackage,
                     @Nullable SuspendParams suspendParams) {
                 if (mUserState != null) {
                     mUserState.putSuspendParams(suspendingPackage, suspendParams);
@@ -359,7 +360,7 @@
 
             @NonNull
             @Override
-            public PackageUserStateWrite removeSuspension(@NonNull String suspendingPackage) {
+            public PackageUserStateWrite removeSuspension(@NonNull UserPackage suspendingPackage) {
                 if (mUserState != null) {
                     mUserState.removeSuspension(suspendingPackage);
                 }
diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java
index 0c6c672..f6b2104 100644
--- a/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java
+++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 
 import com.android.server.pm.pkg.PackageUserStateImpl;
@@ -38,11 +39,11 @@
             @PackageManager.DistractionRestriction int restrictionFlags);
 
     @NonNull
-    PackageUserStateWrite putSuspendParams(@NonNull String suspendingPackage,
+    PackageUserStateWrite putSuspendParams(@NonNull UserPackage suspendingPackage,
             @Nullable SuspendParams suspendParams);
 
     @NonNull
-    PackageUserStateWrite removeSuspension(@NonNull String suspendingPackage);
+    PackageUserStateWrite removeSuspension(@NonNull UserPackage suspendingPackage);
 
     @NonNull
     PackageUserStateWrite setHidden(boolean hidden);
diff --git a/services/core/java/com/android/server/policy/DeferredKeyActionExecutor.java b/services/core/java/com/android/server/policy/DeferredKeyActionExecutor.java
index b531b0e..611e4ed 100644
--- a/services/core/java/com/android/server/policy/DeferredKeyActionExecutor.java
+++ b/services/core/java/com/android/server/policy/DeferredKeyActionExecutor.java
@@ -76,6 +76,18 @@
         getActionsBufferWithLazyCleanUp(keyCode, downTime).setExecutable();
     }
 
+    /**
+     * Clears all the queued action for given key code.
+     *
+     * @param keyCode the key code whose queued actions will be cleared.
+     */
+    public void cancelQueuedAction(int keyCode) {
+        TimedActionsBuffer actionsBuffer = mBuffers.get(keyCode);
+        if (actionsBuffer != null) {
+            actionsBuffer.clear();
+        }
+    }
+
     private TimedActionsBuffer getActionsBufferWithLazyCleanUp(int keyCode, long downTime) {
         TimedActionsBuffer buffer = mBuffers.get(keyCode);
         if (buffer == null || buffer.getDownTime() != downTime) {
@@ -146,6 +158,10 @@
             mActions.clear();
         }
 
+        void clear() {
+            mActions.clear();
+        }
+
         void dump(String prefix, PrintWriter pw) {
             if (mExecutable) {
                 pw.println(prefix + "  " + KeyEvent.keyCodeToString(mKeyCode) + ": executable");
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 938ed23..3000a1c 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -103,6 +103,7 @@
 import android.app.ActivityManager.RecentTaskInfo;
 import android.app.ActivityManagerInternal;
 import android.app.ActivityTaskManager;
+import android.app.ActivityTaskManager.RootTaskInfo;
 import android.app.AppOpsManager;
 import android.app.IActivityManager;
 import android.app.IUiModeManager;
@@ -584,6 +585,10 @@
     private int mLongPressOnStemPrimaryBehavior;
     private RecentTaskInfo mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp;
 
+    // The focused task at the time when the first STEM_PRIMARY key was released. This can only
+    // be accessed from the looper thread.
+    private RootTaskInfo mFocusedTaskInfoOnStemPrimarySingleKeyUp;
+
     private boolean mHandleVolumeKeysInWM;
 
     private boolean mPendingKeyguardOccluded;
@@ -2135,12 +2140,10 @@
     static class Injector {
         private final Context mContext;
         private final WindowManagerFuncs mWindowManagerFuncs;
-        private final Looper mLooper;
 
-        Injector(Context context, WindowManagerFuncs funcs, Looper looper) {
+        Injector(Context context, WindowManagerFuncs funcs) {
             mContext = context;
             mWindowManagerFuncs = funcs;
-            mLooper = looper;
         }
 
         Context getContext() {
@@ -2152,7 +2155,7 @@
         }
 
         Looper getLooper() {
-            return mLooper;
+            return Looper.myLooper();
         }
 
         AccessibilityShortcutController getAccessibilityShortcutController(
@@ -2195,7 +2198,7 @@
     /** {@inheritDoc} */
     @Override
     public void init(Context context, WindowManagerFuncs funcs) {
-        init(new Injector(context, funcs, Looper.myLooper()));
+        init(new Injector(context, funcs));
     }
 
     @VisibleForTesting
@@ -2723,7 +2726,7 @@
 
         @Override
         void onPress(long downTime, int unusedDisplayId) {
-            if (mShouldEarlyShortPressOnStemPrimary) {
+            if (shouldHandleStemPrimaryEarlyShortPress()) {
                 return;
             }
             // Short-press should be triggered only if app doesn't handle it.
@@ -2747,6 +2750,11 @@
             if (count == 3
                     && mTriplePressOnStemPrimaryBehavior
                     == TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY) {
+                // Cancel any queued actions for current key code to prevent them from being
+                // launched after a11y layer enabled. If the action happens early,
+                // undoEarlySinglePress will make sure the correct task is on top.
+                mDeferredKeyActionExecutor.cancelQueuedAction(KeyEvent.KEYCODE_STEM_PRIMARY);
+                undoEarlySinglePress();
                 stemPrimaryPress(count);
             } else {
                 // Other multi-press gestures should be triggered only if app doesn't handle it.
@@ -2755,6 +2763,27 @@
             }
         }
 
+        /**
+         * This method undo the previously launched early-single-press action by bringing the
+         * focused task before launching early-single-press back to top.
+         */
+        private void undoEarlySinglePress() {
+            if (shouldHandleStemPrimaryEarlyShortPress()
+                    && mFocusedTaskInfoOnStemPrimarySingleKeyUp != null) {
+                try {
+                    mActivityManagerService.startActivityFromRecents(
+                            mFocusedTaskInfoOnStemPrimarySingleKeyUp.taskId, null);
+                } catch (RemoteException | IllegalArgumentException e) {
+                    Slog.e(
+                            TAG,
+                            "Failed to start task "
+                                    + mFocusedTaskInfoOnStemPrimarySingleKeyUp.taskId
+                                    + " from recents",
+                            e);
+                }
+            }
+        }
+
         @Override
         void onKeyUp(long eventTime, int count, int unusedDisplayId) {
             if (count == 1) {
@@ -2763,15 +2792,49 @@
                 // It is possible that we may navigate away from this task before the double
                 // press is detected, as a result of the first press, so we save the  current
                 // most recent task before that happens.
+                // TODO(b/311497918): guard this with DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP
                 mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp =
                         mActivityTaskManagerInternal.getMostRecentTaskFromBackground();
-                if (mShouldEarlyShortPressOnStemPrimary) {
+
+                mFocusedTaskInfoOnStemPrimarySingleKeyUp = null;
+
+                if (shouldHandleStemPrimaryEarlyShortPress()) {
                     // Key-up gesture should be triggered only if app doesn't handle it.
                     mDeferredKeyActionExecutor.queueKeyAction(
-                            KeyEvent.KEYCODE_STEM_PRIMARY, eventTime, () -> stemPrimaryPress(1));
+                            KeyEvent.KEYCODE_STEM_PRIMARY,
+                            eventTime,
+                            () -> {
+                                // Save the info of the focused task on screen. This may be used
+                                // later to bring the current focused task back to top. For
+                                // example, stem primary triple press enables the A11y interface
+                                // on top of the current focused task. When early single press is
+                                // enabled for stem primary, the focused task could change to
+                                // something else upon first key up event. In that case, we will
+                                // bring the task recorded by this variable back to top. Then, start
+                                // A11y interface.
+                                try {
+                                    mFocusedTaskInfoOnStemPrimarySingleKeyUp =
+                                            mActivityManagerService.getFocusedRootTaskInfo();
+                                } catch (RemoteException e) {
+                                    Slog.e(
+                                            TAG,
+                                            "StemPrimaryKeyRule: onKeyUp: error while getting "
+                                                    + "focused task "
+                                                    + "info.",
+                                            e);
+                                }
+
+                                stemPrimaryPress(1);
+                            });
                 }
             }
         }
+
+        // TODO(b/311497918): make a shouldHandlePowerEarlyShortPress for power button.
+        private boolean shouldHandleStemPrimaryEarlyShortPress() {
+            return mShouldEarlyShortPressOnStemPrimary
+                    && mShortPressOnStemPrimaryBehavior == SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
+        }
     }
 
     private void initSingleKeyGestureRules(Looper looper) {
@@ -6865,13 +6928,13 @@
     static class ButtonOverridePermissionChecker {
         boolean canAppOverrideSystemKey(Context context, int uid) {
             return PermissionChecker.checkPermissionForDataDelivery(
-                            context,
-                            OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW,
-                            PID_UNKNOWN,
-                            uid,
-                            null,
-                            null,
-                            null)
+                    context,
+                    OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW,
+                    PID_UNKNOWN,
+                    uid,
+                    null,
+                    null,
+                    null)
                     == PERMISSION_GRANTED;
         }
     }
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 7c833cb..6f75439 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -481,6 +481,11 @@
         protected long mTargetDurationNanos;
         protected boolean mUpdateAllowed;
         protected int[] mNewThreadIds;
+        protected boolean mPowerEfficient;
+
+        private enum SessionModes {
+            POWER_EFFICIENCY,
+        };
 
         protected AppHintSession(
                 int uid, int pid, int[] threadIds, IBinder token,
@@ -492,6 +497,7 @@
             mHalSessionPtr = halSessionPtr;
             mTargetDurationNanos = durationNanos;
             mUpdateAllowed = true;
+            mPowerEfficient = false;
             final boolean allowed = mUidObserver.isUidForeground(mUid);
             updateHintAllowed(allowed);
             try {
@@ -634,6 +640,9 @@
                 }
                 Preconditions.checkArgument(mode >= 0, "the mode Id value should be"
                         + " greater than zero.");
+                if (mode == SessionModes.POWER_EFFICIENCY.ordinal()) {
+                    mPowerEfficient = enabled;
+                }
                 mNativeWrapper.halSetMode(mHalSessionPtr, mode, enabled);
             }
         }
@@ -653,6 +662,12 @@
             }
         }
 
+        public boolean isPowerEfficient() {
+            synchronized (this) {
+                return mPowerEfficient;
+            }
+        }
+
         void validateWorkDuration(WorkDuration workDuration) {
             if (DEBUG) {
                 Slogf.d(TAG, "WorkDuration(" + workDuration.getTimestampNanos() + ", "
@@ -718,6 +733,7 @@
                 pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds));
                 pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos);
                 pw.println(prefix + "SessionAllowed: " + mUpdateAllowed);
+                pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false"));
             }
         }
 
diff --git a/services/core/java/com/android/server/utils/WatchedSparseSetArray.java b/services/core/java/com/android/server/utils/WatchedSparseSetArray.java
index b8850af..ff426cc 100644
--- a/services/core/java/com/android/server/utils/WatchedSparseSetArray.java
+++ b/services/core/java/com/android/server/utils/WatchedSparseSetArray.java
@@ -47,6 +47,17 @@
     }
 
     /**
+     * Create a new WatchedSparseSetArray from an existing SparseSetArray without copying.
+     * <p>
+     * Use with caution: Callers must ensure that no reference to {@code sparseSetArray} exists
+     * anywhere else in the system. If such a reference does exist, then changes to the storage via
+     * that reference will not be noticed by the new WatchedSpareSetArray.
+     */
+    public WatchedSparseSetArray(@NonNull SparseSetArray<T> sparseSetArray) {
+        mStorage = sparseSetArray;
+    }
+
+    /**
      * Return the underlying storage.  This breaks the wrapper but is necessary when
      * passing the array to distant methods.
      */
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2df965c..145eb3b 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -125,6 +125,7 @@
 import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_OLD_UNSET;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
+import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION;
 import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
@@ -182,6 +183,7 @@
 import static com.android.server.wm.ActivityRecordProto.REPORTED_DRAWN;
 import static com.android.server.wm.ActivityRecordProto.REPORTED_VISIBLE;
 import static com.android.server.wm.ActivityRecordProto.SHOULD_FORCE_ROTATE_FOR_CAMERA_COMPAT;
+import static com.android.server.wm.ActivityRecordProto.SHOULD_OVERRIDE_MIN_ASPECT_RATIO;
 import static com.android.server.wm.ActivityRecordProto.SHOULD_REFRESH_ACTIVITY_FOR_CAMERA_COMPAT;
 import static com.android.server.wm.ActivityRecordProto.SHOULD_REFRESH_ACTIVITY_VIA_PAUSE_FOR_CAMERA_COMPAT;
 import static com.android.server.wm.ActivityRecordProto.SHOULD_SEND_COMPAT_FAKE_FOCUS;
@@ -564,7 +566,6 @@
     boolean idle;           // has the activity gone idle?
     boolean hasBeenLaunched;// has this activity ever been launched?
     boolean immersive;      // immersive mode (don't interrupt if possible)
-    boolean forceNewConfig; // force re-create with new config next time
     boolean supportsEnterPipOnTaskSwitch;  // This flag is set by the system to indicate that the
         // activity can enter picture in picture while pausing (only when switching to another task)
     // The PiP params used when deferring the entering of picture-in-picture.
@@ -683,6 +684,7 @@
     private final WindowState.UpdateReportedVisibilityResults mReportedVisibilityResults =
             new WindowState.UpdateReportedVisibilityResults();
 
+    // TODO(b/317000737): Replace it with visibility states lookup.
     int mTransitionChangeFlags;
 
     /** Whether we need to setup the animation to animate only within the letterbox. */
@@ -5468,8 +5470,13 @@
         // Defer committing visibility until transition starts.
         if (isCollecting) {
             // It may be occluded by the activity above that calls convertFromTranslucent().
-            if (!visible && mTransitionController.inPlayingTransition(this)) {
-                mTransitionChangeFlags |= FLAG_IS_OCCLUDED;
+            // Or it may be restoring transient launch to invisible when finishing transition.
+            if (!visible) {
+                if (mTransitionController.inPlayingTransition(this)) {
+                    mTransitionChangeFlags |= FLAG_IS_OCCLUDED;
+                } else if (mTransitionController.inFinishingTransition(this)) {
+                    mTransitionChangeFlags |= FLAGS_IS_OCCLUDED_NO_ANIMATION;
+                }
             }
             return;
         }
@@ -9599,7 +9606,7 @@
         // configurations because there are cases (like moving a task to the root pinned task) where
         // the combine configurations are equal, but would otherwise differ in the override config
         mTmpConfig.setTo(mLastReportedConfiguration.getMergedConfiguration());
-        if (getConfiguration().equals(mTmpConfig) && !forceNewConfig && !displayChanged) {
+        if (getConfiguration().equals(mTmpConfig) && !displayChanged) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration & display "
                     + "unchanged in %s", this);
             return true;
@@ -9626,7 +9633,7 @@
             return true;
         }
 
-        if (changes == 0 && !forceNewConfig) {
+        if (changes == 0) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration no differences in %s",
                     this);
             // There are no significant differences, so we won't relaunch but should still deliver
@@ -9648,7 +9655,6 @@
         // pick that up next time it starts.
         if (!attachedToProcess()) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration doesn't matter not running %s", this);
-            forceNewConfig = false;
             return true;
         }
 
@@ -9658,11 +9664,10 @@
                 Integer.toHexString(changes), Integer.toHexString(info.getRealConfigChanged()),
                 mLastReportedConfiguration);
 
-        if (shouldRelaunchLocked(changes, mTmpConfig) || forceNewConfig) {
+        if (shouldRelaunchLocked(changes, mTmpConfig)) {
             // Aha, the activity isn't handling the change, so DIE DIE DIE.
             configChangeFlags |= changes;
             startFreezingScreenLocked(globalChanges);
-            forceNewConfig = false;
             // Do not preserve window if it is freezing screen because the original window won't be
             // able to update drawn state that causes freeze timeout.
             preserveWindow &= isResizeOnlyChange(changes) && !mFreezingScreen;
@@ -9882,7 +9887,6 @@
         try {
             ProtoLog.i(WM_DEBUG_STATES, "Moving to %s Relaunching %s callers=%s" ,
                     (andResume ? "RESUMED" : "PAUSED"), this, Debug.getCallers(6));
-            forceNewConfig = false;
             final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(token,
                     pendingResults, pendingNewIntents, configChangeFlags,
                     new MergedConfiguration(getProcessGlobalConfiguration(),
@@ -10357,6 +10361,8 @@
                 mLetterboxUiController.shouldRefreshActivityForCameraCompat());
         proto.write(SHOULD_REFRESH_ACTIVITY_VIA_PAUSE_FOR_CAMERA_COMPAT,
                 mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat());
+        proto.write(SHOULD_OVERRIDE_MIN_ASPECT_RATIO,
+                mLetterboxUiController.shouldOverrideMinAspectRatio());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 86be6ba..7af494c 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -33,6 +33,7 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
+import com.android.window.flags.Flags;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -121,7 +122,8 @@
 
     // TODO remove when enabled
     static boolean isSnapshotEnabled() {
-        return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0;
+        return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0
+                || Flags.activitySnapshotByDefault();
     }
 
     static PersistInfoProvider createPersistInfoProvider(
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index f9d344b..e7621ff 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -48,6 +48,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -223,7 +224,7 @@
             // before issuing the work challenge.
             return true;
         }
-        if (interceptLockedManagedProfileIfNeeded()) {
+        if (interceptLockedProfileIfNeeded()) {
             return true;
         }
         if (interceptHomeIfNeeded()) {
@@ -335,19 +336,19 @@
             return false;
         }
         final String suspendedPackage = mAInfo.applicationInfo.packageName;
-        final String suspendingPackage = pmi.getSuspendingPackage(suspendedPackage, mUserId);
-        if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) {
+        final UserPackage suspender = pmi.getSuspendingPackage(suspendedPackage, mUserId);
+        if (suspender != null && PLATFORM_PACKAGE_NAME.equals(suspender.packageName)) {
             return interceptSuspendedByAdminPackage();
         }
         final SuspendDialogInfo dialogInfo = pmi.getSuspendedDialogInfo(suspendedPackage,
-                suspendingPackage, mUserId);
+                suspender, mUserId);
         final Bundle crossProfileOptions = hasCrossProfileAnimation()
                 ? ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle()
                 : null;
         final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
                 FLAG_IMMUTABLE);
         mIntent = SuspendedAppActivity.createSuspendedAppInterceptIntent(suspendedPackage,
-                suspendingPackage, dialogInfo, crossProfileOptions, target, mUserId);
+                suspender, dialogInfo, crossProfileOptions, target, mUserId);
         mCallingPid = mRealCallingPid;
         mCallingUid = mRealCallingUid;
         mResolvedType = null;
@@ -377,7 +378,7 @@
         return true;
     }
 
-    private boolean interceptLockedManagedProfileIfNeeded() {
+    private boolean interceptLockedProfileIfNeeded() {
         final Intent interceptingIntent = interceptWithConfirmCredentialsIfNeeded(mAInfo, mUserId);
         if (interceptingIntent == null) {
             return false;
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 630b9e1..cb2adbc 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -579,30 +579,11 @@
                     computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid),
                     realCallingPid);
             if (resolveInfo == null) {
-                final UserInfo userInfo = supervisor.getUserInfo(userId);
-                if (userInfo != null && userInfo.isManagedProfile()) {
-                    // Special case for managed profiles, if attempting to launch non-cryto aware
-                    // app in a locked managed profile from an unlocked parent allow it to resolve
-                    // as user will be sent via confirm credentials to unlock the profile.
-                    final UserManager userManager = UserManager.get(supervisor.mService.mContext);
-                    boolean profileLockedAndParentUnlockingOrUnlocked = false;
-                    final long token = Binder.clearCallingIdentity();
-                    try {
-                        final UserInfo parent = userManager.getProfileParent(userId);
-                        profileLockedAndParentUnlockingOrUnlocked = (parent != null)
-                                && userManager.isUserUnlockingOrUnlocked(parent.id)
-                                && !userManager.isUserUnlockingOrUnlocked(userId);
-                    } finally {
-                        Binder.restoreCallingIdentity(token);
-                    }
-                    if (profileLockedAndParentUnlockingOrUnlocked) {
-                        resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId,
-                                PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
-                                computeResolveFilterUid(callingUid, realCallingUid,
-                                        filterCallingUid), realCallingPid);
-                    }
-                }
+                // Special case for profiles: If attempting to launch non-crypto aware app in a
+                // locked profile or launch an app in a profile that is stopped by quiet mode from
+                // an unlocked parent, allow it to resolve as user will be sent via confirm
+                // credentials to unlock the profile.
+                resolveInfo = resolveIntentForLockedOrStoppedProfiles(supervisor);
             }
 
             // Collect information about the target of the Intent.
@@ -616,6 +597,36 @@
                         UserHandle.getUserId(activityInfo.applicationInfo.uid));
             }
         }
+
+        /**
+         * Resolve intent for locked or stopped profiles if the parent profile is unlocking or
+         * unlocked.
+         */
+        ResolveInfo resolveIntentForLockedOrStoppedProfiles(
+                ActivityTaskSupervisor supervisor) {
+            final UserInfo userInfo = supervisor.getUserInfo(userId);
+            if (userInfo != null && userInfo.isProfile()) {
+                final UserManager userManager = UserManager.get(supervisor.mService.mContext);
+                boolean profileLockedAndParentUnlockingOrUnlocked = false;
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    final UserInfo parent = userManager.getProfileParent(userId);
+                    profileLockedAndParentUnlockingOrUnlocked = (parent != null)
+                            && userManager.isUserUnlockingOrUnlocked(parent.id)
+                            && !userManager.isUserUnlockingOrUnlocked(userId);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+                if (profileLockedAndParentUnlockingOrUnlocked) {
+                    return supervisor.resolveIntent(intent, resolvedType, userId,
+                            PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                            computeResolveFilterUid(callingUid, realCallingUid,
+                                    filterCallingUid), realCallingPid);
+                }
+            }
+            return null;
+        }
     }
 
     ActivityStarter(ActivityStartController controller, ActivityTaskManagerService service,
@@ -2767,10 +2778,7 @@
             }
         }
 
-        // If the target task is not in the front, then we need to bring it to the front...
-        // except...  well, with SINGLE_TASK_LAUNCH it's not entirely clear. We'd like to have
-        // the same behavior as if a new instance was being started, which means not bringing it
-        // to the front if the caller is not itself in the front.
+        // If the target task is not in the front, then we need to bring it to the front.
         final boolean differentTopTask;
         if (mTargetRootTask.getDisplayArea() == mPreferredTaskDisplayArea) {
             final Task focusRootTask = mTargetRootTask.mDisplayContent.getFocusedRootTask();
@@ -2787,49 +2795,47 @@
 
         if (differentTopTask && !avoidMoveToFront()) {
             mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
-            if (mSourceRecord == null || inTopNonFinishingTask(mSourceRecord)) {
-                // We really do want to push this one into the user's face, right now.
-                if (mLaunchTaskBehind && mSourceRecord != null) {
-                    intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask());
-                }
-
-                if (intentActivity.isDescendantOf(mTargetRootTask)) {
-                    // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels
-                    //  tasks hierarchies.
-                    if (mTargetRootTask != intentTask
-                            && mTargetRootTask != intentTask.getParent().asTask()) {
-                        intentTask.getParent().positionChildAt(POSITION_TOP, intentTask,
-                                false /* includingParents */);
-                        intentTask = intentTask.getParent().asTaskFragment().getTask();
-                    }
-                    // If the activity is visible in multi-windowing mode, it may already be on
-                    // the top (visible to user but not the global top), then the result code
-                    // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
-                    final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
-                            && intentActivity.inMultiWindowMode()
-                            && intentActivity == mTargetRootTask.topRunningActivity()
-                            && !intentActivity.mTransitionController.isTransientHide(
-                                    mTargetRootTask);
-                    // We only want to move to the front, if we aren't going to launch on a
-                    // different root task. If we launch on a different root task, we will put the
-                    // task on top there.
-                    // Defer resuming the top activity while moving task to top, since the
-                    // current task-top activity may not be the activity that should be resumed.
-                    mTargetRootTask.moveTaskToFront(intentTask, mNoAnimation, mOptions,
-                            mStartActivity.appTimeTracker, DEFER_RESUME,
-                            "bringingFoundTaskToFront");
-                    mMovedToFront = !wasTopOfVisibleRootTask;
-                } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) {
-                    // Leaves reparenting pinned task operations to task organizer to make sure it
-                    // dismisses pinned task properly.
-                    // TODO(b/199997762): Consider leaving all reparent operation of organized tasks
-                    //  to task organizer.
-                    intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
-                            ANIMATE, DEFER_RESUME, "reparentToTargetRootTask");
-                    mMovedToFront = true;
-                }
-                mOptions = null;
+            // We really do want to push this one into the user's face, right now.
+            if (mLaunchTaskBehind && mSourceRecord != null) {
+                intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask());
             }
+
+            if (intentActivity.isDescendantOf(mTargetRootTask)) {
+                // TODO(b/151572268): Figure out a better way to move tasks in above 2-levels
+                //  tasks hierarchies.
+                if (mTargetRootTask != intentTask
+                        && mTargetRootTask != intentTask.getParent().asTask()) {
+                    intentTask.getParent().positionChildAt(POSITION_TOP, intentTask,
+                            false /* includingParents */);
+                    intentTask = intentTask.getParent().asTaskFragment().getTask();
+                }
+                // If the activity is visible in multi-windowing mode, it may already be on
+                // the top (visible to user but not the global top), then the result code
+                // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
+                final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
+                        && intentActivity.inMultiWindowMode()
+                        && intentActivity == mTargetRootTask.topRunningActivity()
+                        && !intentActivity.mTransitionController.isTransientHide(
+                                mTargetRootTask);
+                // We only want to move to the front, if we aren't going to launch on a
+                // different root task. If we launch on a different root task, we will put the
+                // task on top there.
+                // Defer resuming the top activity while moving task to top, since the
+                // current task-top activity may not be the activity that should be resumed.
+                mTargetRootTask.moveTaskToFront(intentTask, mNoAnimation, mOptions,
+                        mStartActivity.appTimeTracker, DEFER_RESUME,
+                        "bringingFoundTaskToFront");
+                mMovedToFront = !wasTopOfVisibleRootTask;
+            } else if (intentActivity.getWindowingMode() != WINDOWING_MODE_PINNED) {
+                // Leaves reparenting pinned task operations to task organizer to make sure it
+                // dismisses pinned task properly.
+                // TODO(b/199997762): Consider leaving all reparent operation of organized tasks
+                //  to task organizer.
+                intentTask.reparent(mTargetRootTask, ON_TOP, REPARENT_MOVE_ROOT_TASK_TO_FRONT,
+                        ANIMATE, DEFER_RESUME, "reparentToTargetRootTask");
+                mMovedToFront = true;
+            }
+            mOptions = null;
         }
         if (differentTopTask) {
             logPIOnlyCreatorAllowsBAL();
@@ -2850,20 +2856,6 @@
                 mRootWindowContainer.getDefaultTaskDisplayArea(), mTargetRootTask);
     }
 
-    private boolean inTopNonFinishingTask(ActivityRecord r) {
-        if (r == null || r.getTask() == null) {
-            return false;
-        }
-
-        final Task rTask = r.getTask();
-        final Task parent = rTask.getCreatedByOrganizerTask() != null
-                ? rTask.getCreatedByOrganizerTask() : r.getRootTask();
-        final ActivityRecord topNonFinishingActivity = parent != null
-                ? parent.getTopNonFinishingActivity() : null;
-
-        return topNonFinishingActivity != null && topNonFinishingActivity.getTask() == rTask;
-    }
-
     private void resumeTargetRootTaskIfNeeded() {
         if (mDoResume) {
             final ActivityRecord next = mTargetRootTask.topRunningActivity(
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 869bcc0..dbae29b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -62,6 +62,7 @@
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RTL;
 import static android.provider.Settings.Global.HIDE_ERROR_DIALOGS;
 import static android.provider.Settings.System.FONT_SCALE;
+import static android.service.controls.flags.Flags.homePanelDream;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -1501,14 +1502,19 @@
         a.exported = true;
         a.name = DreamActivity.class.getName();
         a.enabled = true;
-        a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE;
         a.persistableMode = ActivityInfo.PERSIST_NEVER;
         a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
         a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
-        a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
         a.configChanges = 0xffffffff;
 
+        if (homePanelDream()) {
+            a.launchMode = ActivityInfo.LAUNCH_SINGLE_TASK;
+        } else {
+            a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
+            a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE;
+        }
+
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchActivityType(ACTIVITY_TYPE_DREAM);
 
@@ -3695,6 +3701,21 @@
         boolean originallyFromClient = fromClient
                 && (!r.isState(PAUSING) || params.isAutoEnterEnabled());
 
+        // If PiP2 flag is on and client-request to enter PiP came via onUserLeaveHint(),
+        // we request a direct transition from Shell to TRANSIT_PIP_LEGACY to get the startWct
+        // with the right entry bounds.
+        if (isPip2ExperimentEnabled() && !originallyFromClient && !params.isAutoEnterEnabled()) {
+            final Transition legacyEnterPipTransition = new Transition(TRANSIT_PIP,
+                    0 /* flags */, getTransitionController(),
+                    mWindowManager.mSyncEngine);
+            legacyEnterPipTransition.setPipActivity(r);
+            getTransitionController().startCollectOrQueue(legacyEnterPipTransition, (deferred) -> {
+                getTransitionController().requestStartTransition(legacyEnterPipTransition,
+                        r.getTask(), null /* remoteTransition */, null /* displayChange */);
+            });
+            return true;
+        }
+
         // Create a transition only for this pip entry if it is coming from the app without the
         // system requesting that the app enter-pip. If the system requested it, that means it
         // should be part of that transition if possible.
@@ -4762,6 +4783,10 @@
         if (DEBUG_ALL && !mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) {
             Slog.i(TAG, "continueWindowLayout reason=" + mLayoutReasons);
         }
+
+        // ClientTransactions is queued during #deferWindowLayout() for performance.
+        // Notify to continue.
+        mLifecycleManager.onLayoutContinued();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index e59601c..10efb94 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -906,7 +906,6 @@
                 }
                 mService.getPackageManagerInternalLocked().notifyPackageUse(
                         r.intent.getComponent().getPackageName(), NOTIFY_PACKAGE_USE_ACTIVITY);
-                r.forceNewConfig = false;
                 mService.getAppWarningsLocked().onStartActivity(r);
 
                 // Because we could be starting an Activity in the system process this may not go
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index 68d13cd..6ed8967 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -91,6 +91,13 @@
     /** Non-zero if this controller is triggered by shell transition. */
     private final @TransitionOp int mTransitionOp;
 
+    /**
+     * Whether {@link #setupStartTransaction} is called when the transition is ready.
+     * If this is never set for {@link #OP_CHANGE}, the display may be changed to original state
+     * before the transition is ready, then this controller should be finished.
+     */
+    private boolean mIsStartTransactionPrepared;
+
     /** Whether the start transaction of the transition is committed (by shell). */
     private boolean mIsStartTransactionCommitted;
 
@@ -226,7 +233,8 @@
     void updateTargetWindows() {
         if (mTransitionOp == OP_LEGACY) return;
         if (!mIsStartTransactionCommitted) {
-            if (mTimeoutRunnable == null && !mDisplayContent.hasTopFixedRotationLaunchingApp()
+            if ((mTimeoutRunnable == null || !mIsStartTransactionPrepared)
+                    && !mDisplayContent.hasTopFixedRotationLaunchingApp()
                     && !mDisplayContent.isRotationChanging() && !mDisplayContent.inTransition()) {
                 Slog.d(TAG, "Cancel for no change");
                 mDisplayContent.finishAsyncRotationIfPossible();
@@ -401,9 +409,18 @@
         if (mTimeoutRunnable == null) {
             mTimeoutRunnable = () -> {
                 synchronized (mService.mGlobalLock) {
-                    Slog.i(TAG, "Async rotation timeout: " + (!mIsStartTransactionCommitted
-                            ? " start transaction is not committed" : mTargetWindowTokens));
+                    final String reason;
                     if (!mIsStartTransactionCommitted) {
+                        if (!mIsStartTransactionPrepared) {
+                            reason = "setupStartTransaction is not called";
+                        } else {
+                            reason = "start transaction is not committed";
+                        }
+                    } else {
+                        reason = "unfinished windows " + mTargetWindowTokens;
+                    }
+                    Slog.i(TAG, "Async rotation timeout: " + reason);
+                    if (!mIsStartTransactionCommitted && mIsStartTransactionPrepared) {
                         // The transaction commit timeout will be handled by:
                         // 1. BLASTSyncEngine will notify onTransactionCommitTimeout() and then
                         //    apply the start transaction of transition.
@@ -558,6 +575,7 @@
                 }
             }
         });
+        mIsStartTransactionPrepared = true;
     }
 
     /** Called when the start transition is ready, but it is not applied in time. */
@@ -577,6 +595,10 @@
     /** Called when the transition by shell is done. */
     void onTransitionFinished() {
         if (mTransitionOp == OP_CHANGE) {
+            if (mTargetWindowTokens.isEmpty()) {
+                // If nothing was handled, then complete with the transition.
+                mDisplayContent.finishAsyncRotationIfPossible();
+            }
             // With screen rotation animation, the windows are always faded in when they are drawn.
             // Because if they are drawn fast enough, the fade animation should not be observable.
             return;
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 4929df80..eed46fe 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -36,6 +36,7 @@
 import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static java.util.Objects.requireNonNull;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -152,36 +153,25 @@
     static final int BAL_ALLOW_SDK_SANDBOX = 10;
 
     static String balCodeToString(@BalCode int balCode) {
-        switch (balCode) {
-            case BAL_ALLOW_ALLOWLISTED_COMPONENT:
-                return "BAL_ALLOW_ALLOWLISTED_COMPONENT";
-            case BAL_ALLOW_ALLOWLISTED_UID:
-                return "BAL_ALLOW_ALLOWLISTED_UID";
-            case BAL_ALLOW_DEFAULT:
-                return "BAL_ALLOW_DEFAULT";
-            case BAL_ALLOW_FOREGROUND:
-                return "BAL_ALLOW_FOREGROUND";
-            case BAL_ALLOW_GRACE_PERIOD:
-                return "BAL_ALLOW_GRACE_PERIOD";
-            case BAL_ALLOW_PENDING_INTENT:
-                return "BAL_ALLOW_PENDING_INTENT";
-            case BAL_ALLOW_PERMISSION:
-                return "BAL_ALLOW_PERMISSION";
-            case BAL_ALLOW_SAW_PERMISSION:
-                return "BAL_ALLOW_SAW_PERMISSION";
-            case BAL_ALLOW_SDK_SANDBOX:
-                return "BAL_ALLOW_SDK_SANDBOX";
-            case BAL_ALLOW_VISIBLE_WINDOW:
-                return "BAL_ALLOW_VISIBLE_WINDOW";
-            case BAL_BLOCK:
-                return "BAL_BLOCK";
-            default:
-                throw new IllegalArgumentException("Unexpected value: " + balCode);
-        }
+        return switch (balCode) {
+            case BAL_ALLOW_ALLOWLISTED_COMPONENT -> "BAL_ALLOW_ALLOWLISTED_COMPONENT";
+            case BAL_ALLOW_ALLOWLISTED_UID -> "BAL_ALLOW_ALLOWLISTED_UID";
+            case BAL_ALLOW_DEFAULT -> "BAL_ALLOW_DEFAULT";
+            case BAL_ALLOW_FOREGROUND -> "BAL_ALLOW_FOREGROUND";
+            case BAL_ALLOW_GRACE_PERIOD -> "BAL_ALLOW_GRACE_PERIOD";
+            case BAL_ALLOW_PENDING_INTENT -> "BAL_ALLOW_PENDING_INTENT";
+            case BAL_ALLOW_PERMISSION -> "BAL_ALLOW_PERMISSION";
+            case BAL_ALLOW_SAW_PERMISSION -> "BAL_ALLOW_SAW_PERMISSION";
+            case BAL_ALLOW_SDK_SANDBOX -> "BAL_ALLOW_SDK_SANDBOX";
+            case BAL_ALLOW_VISIBLE_WINDOW -> "BAL_ALLOW_VISIBLE_WINDOW";
+            case BAL_BLOCK -> "BAL_BLOCK";
+            default -> throw new IllegalArgumentException("Unexpected value: " + balCode);
+        };
     }
 
     @GuardedBy("mService.mGlobalLock")
-    private HashMap<Integer, FinishedActivityEntry> mTaskIdToFinishedActivity = new HashMap<>();
+    private final HashMap<Integer, FinishedActivityEntry> mTaskIdToFinishedActivity =
+            new HashMap<>();
     @GuardedBy("mService.mGlobalLock")
     private FinishedActivityEntry mTopFinishedActivity = null;
 
@@ -467,9 +457,8 @@
             return !blocks();
         }
 
-        BalVerdict setOnlyCreatorAllows(boolean onlyCreatorAllows) {
+        void setOnlyCreatorAllows(boolean onlyCreatorAllows) {
             mOnlyCreatorAllows = onlyCreatorAllows;
-            return this;
         }
 
         boolean onlyCreatorAllows() {
@@ -481,10 +470,6 @@
             return this;
         }
 
-        private boolean isBasedOnRealCaller() {
-            return mBasedOnRealCaller;
-        }
-
         public String toString() {
             StringBuilder builder = new StringBuilder();
             builder.append(balCodeToString(mCode));
@@ -583,15 +568,14 @@
         BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state);
 
         if (!state.hasRealCaller()) {
-            BalVerdict resultForRealCaller = null; // nothing to compute
             if (resultForCaller.allows()) {
                 if (DEBUG_ACTIVITY_STARTS) {
                     Slog.d(TAG, "Background activity start allowed. "
-                            + state.dump(resultForCaller, resultForRealCaller));
+                            + state.dump(resultForCaller, resultForCaller));
                 }
                 return statsLog(resultForCaller, state);
             }
-            return abortLaunch(state, resultForCaller, resultForRealCaller);
+            return abortLaunch(state, resultForCaller, resultForCaller);
         }
 
         // The realCaller result is only calculated for PendingIntents (indicated by a valid
@@ -653,7 +637,7 @@
                                 + " if the PI creator upgrades target_sdk to 35+"
                                 + " AND the PI sender upgrades target_sdk to 34+! "
                                 + state.dump(resultForCaller, resultForRealCaller));
-                showBalRiskToast("BAL would be blocked", state);
+                showBalRiskToast();
                 // return the realCaller result for backwards compatibility
                 return statsLog(resultForRealCaller, state);
             }
@@ -679,7 +663,7 @@
                                 + " if the PI creator upgrades target_sdk to 35+! "
                                 + " (missing opt in by PI creator)! "
                                 + state.dump(resultForCaller, resultForRealCaller));
-                showBalRiskToast("BAL would be blocked", state);
+                showBalRiskToast();
                 return statsLog(resultForCaller, state);
             }
             Slog.wtf(TAG,
@@ -696,7 +680,7 @@
                                 + " if the PI sender upgrades target_sdk to 34+! "
                                 + " (missing opt in by PI sender)! "
                                 + state.dump(resultForCaller, resultForRealCaller));
-                showBalRiskToast("BAL would be blocked", state);
+                showBalRiskToast();
                 return statsLog(resultForRealCaller, state);
             }
             Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
@@ -712,7 +696,7 @@
             BalVerdict resultForRealCaller) {
         Slog.w(TAG, "Background activity launch blocked! "
                 + state.dump(resultForCaller, resultForRealCaller));
-        showBalBlockedToast("BAL blocked", state);
+        showBalBlockedToast();
         return statsLog(BalVerdict.BLOCK, state);
     }
 
@@ -910,7 +894,7 @@
 
     /**
      * Check if the app allows BAL.
-     *
+     * <p>
      * See {@link BackgroundLaunchProcessController#areBackgroundActivityStartsAllowed(int, int,
      * String, int, boolean, boolean, boolean, long, long, long)} for details on the
      * exceptions.
@@ -1104,19 +1088,15 @@
         return true;
     }
 
-    private void showBalBlockedToast(String toastText, BalState state) {
+    private void showBalBlockedToast() {
         if (balShowToastsBlocked()) {
-            showToast(toastText
-                    + " caller:" + state.mCallingPackage
-                    + " realCaller:" + state.mRealCallingPackage);
+            showToast("BAL blocked. go/debug-bal");
         }
     }
 
-    private void showBalRiskToast(String toastText, BalState state) {
+    private void showBalRiskToast() {
         if (balShowToasts()) {
-            showToast(toastText
-                    + " caller:" + state.mCallingPackage
-                    + " realCaller:" + state.mRealCallingPackage);
+            showToast("BAL allowed in compat mode. go/debug-bal");
         }
     }
 
@@ -1281,7 +1261,7 @@
      * 2. Or top of an adjacent task fragment to (1)
      * <p>
      * The 'sourceRecord' can be considered top even if it is 'finishing'
-     *
+     * <p>
      * Returns a class where the elements are:
      * <pre>
      * shouldBlockActivityStart: {@code true} if we should actually block the transition (takes into
@@ -1344,7 +1324,7 @@
     /**
      * Determines if a source is allowed to add or remove activities from the task,
      * if the current ActivityRecord is above it in the stack
-     *
+     * <p>
      * A transition is blocked ({@code false} returned) if all of the following are met:
      * <pre>
      * 1. The source activity and the current activity record belong to different apps
@@ -1489,8 +1469,8 @@
 
         if (code == BAL_ALLOW_PENDING_INTENT
                 && (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) {
-            String activityName =
-                    intent != null ? intent.getComponent().flattenToShortString() : "";
+            String activityName = intent != null
+                    ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
             FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
                     activityName,
                     BAL_ALLOW_PENDING_INTENT,
diff --git a/services/core/java/com/android/server/wm/ClientLifecycleManager.java b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
index 6b6388b..2e47677 100644
--- a/services/core/java/com/android/server/wm/ClientLifecycleManager.java
+++ b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
@@ -105,7 +105,7 @@
             final ClientTransaction clientTransaction = getOrCreatePendingTransaction(client);
             clientTransaction.addTransactionItem(transactionItem);
 
-            onClientTransactionItemScheduledLocked(clientTransaction);
+            onClientTransactionItemScheduled(clientTransaction);
         } else {
             // TODO(b/260873529): cleanup after launch.
             final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
@@ -134,7 +134,7 @@
             clientTransaction.addTransactionItem(transactionItem);
             clientTransaction.addTransactionItem(lifecycleItem);
 
-            onClientTransactionItemScheduledLocked(clientTransaction);
+            onClientTransactionItemScheduled(clientTransaction);
         } else {
             // TODO(b/260873529): cleanup after launch.
             final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
@@ -146,6 +146,9 @@
 
     /** Executes all the pending transactions. */
     void dispatchPendingTransactions() {
+        if (!Flags.bundleClientTransactionFlag()) {
+            return;
+        }
         final int size = mPendingTransactions.size();
         for (int i = 0; i < size; i++) {
             final ClientTransaction transaction = mPendingTransactions.valueAt(i);
@@ -158,6 +161,20 @@
         mPendingTransactions.clear();
     }
 
+    /**
+     * Called to when {@link WindowSurfacePlacer#continueLayout}.
+     * Dispatches all pending transactions unless there is an ongoing/scheduled layout, in which
+     * case the pending transactions will be dispatched in
+     * {@link RootWindowContainer#performSurfacePlacementNoTrace}.
+     */
+    void onLayoutContinued() {
+        if (shouldDispatchPendingTransactionsImmediately()) {
+            // Dispatch the pending transactions immediately if there is no ongoing/scheduled layout
+            dispatchPendingTransactions();
+        }
+    }
+
+    /** Must only be called with WM lock. */
     @NonNull
     private ClientTransaction getOrCreatePendingTransaction(@NonNull IApplicationThread client) {
         final IBinder clientBinder = client.asBinder();
@@ -173,20 +190,28 @@
     }
 
     /** Must only be called with WM lock. */
-    private void onClientTransactionItemScheduledLocked(
+    private void onClientTransactionItemScheduled(
             @NonNull ClientTransaction clientTransaction) throws RemoteException {
-        // TODO(b/260873529): make sure WindowSurfacePlacer#requestTraversal is called before
-        // ClientTransaction scheduled when needed.
-
-        if (mWms != null && (mWms.mWindowPlacerLocked.isInLayout()
-                || mWms.mWindowPlacerLocked.isTraversalScheduled())) {
-            // The pending transactions will be dispatched when
-            // RootWindowContainer#performSurfacePlacementNoTrace.
-            return;
+        if (shouldDispatchPendingTransactionsImmediately()) {
+            // Dispatch the pending transaction immediately.
+            mPendingTransactions.remove(clientTransaction.getClient().asBinder());
+            scheduleTransaction(clientTransaction);
         }
+    }
 
-        // Dispatch the pending transaction immediately.
-        mPendingTransactions.remove(clientTransaction.getClient().asBinder());
-        scheduleTransaction(clientTransaction);
+    /** Must only be called with WM lock. */
+    private boolean shouldDispatchPendingTransactionsImmediately() {
+        if (mWms == null) {
+            return true;
+        }
+        // Do not dispatch when
+        // 1. Layout deferred.
+        // 2. Layout requested.
+        // 3. Layout in process.
+        // The pending transactions will be dispatched during layout in
+        // RootWindowContainer#performSurfacePlacementNoTrace.
+        return !mWms.mWindowPlacerLocked.isLayoutDeferred()
+                && !mWms.mWindowPlacerLocked.isTraversalScheduled()
+                && !mWms.mWindowPlacerLocked.isInLayout();
     }
 }
diff --git a/services/core/java/com/android/server/wm/CompatModePackages.java b/services/core/java/com/android/server/wm/CompatModePackages.java
index 73bcc8d..1a8927e 100644
--- a/services/core/java/com/android/server/wm/CompatModePackages.java
+++ b/services/core/java/com/android/server/wm/CompatModePackages.java
@@ -19,7 +19,6 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
 import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_FIRST;
 import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_LAST;
 
@@ -47,6 +46,7 @@
 import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.Xml;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -60,6 +60,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
@@ -347,6 +348,7 @@
     private GameManagerInternal mGameManager;
     private final AtomicFile mFile;
     private final HashMap<String, Integer> mPackages = new HashMap<>();
+    private final SparseBooleanArray mLegacyScreenCompatPackages = new SparseBooleanArray();
     private final CompatHandler mHandler;
 
     private final SparseArray<CompatScaleProvider> mProviders = new SparseArray<>();
@@ -427,6 +429,7 @@
             mPackages.remove(packageName);
             scheduleWrite();
         }
+        mLegacyScreenCompatPackages.delete(packageName.hashCode());
     }
 
     public void handlePackageAddedLocked(String packageName, boolean updated) {
@@ -458,6 +461,17 @@
         mHandler.sendMessageDelayed(msg, 10000);
     }
 
+    /**
+     * Returns {@code true} if the windows belonging to the package should be scaled with
+     * {@link DisplayContent#mCompatibleScreenScale}.
+     */
+    boolean useLegacyScreenCompatMode(String packageName) {
+        if (mLegacyScreenCompatPackages.size() == 0) {
+            return false;
+        }
+        return mLegacyScreenCompatPackages.get(packageName.hashCode());
+    }
+
     public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {
         final boolean forceCompat = getPackageCompatModeEnabledLocked(ai);
         final CompatScale compatScale = getCompatScaleFromProvider(ai.packageName, ai.uid);
@@ -466,8 +480,18 @@
                 : getCompatScale(ai.packageName, ai.uid, /* checkProvider= */ false);
         final float densityScale = compatScale != null ? compatScale.mDensityScaleFactor : appScale;
         final Configuration config = mService.getGlobalConfiguration();
-        return new CompatibilityInfo(ai, config.screenLayout, config.smallestScreenWidthDp,
-                forceCompat, appScale, densityScale);
+        final CompatibilityInfo info = new CompatibilityInfo(ai, config.screenLayout,
+                config.smallestScreenWidthDp, forceCompat, appScale, densityScale);
+        // Ignore invalid info which may be a placeholder of isolated process.
+        if (ai.flags != 0 && ai.sourceDir != null) {
+            if (!info.supportsScreen() && !"android".equals(ai.packageName)) {
+                Slog.i(TAG, "Use legacy screen compat mode: " + ai.packageName);
+                mLegacyScreenCompatPackages.put(ai.packageName.hashCode(), true);
+            } else if (mLegacyScreenCompatPackages.size() > 0) {
+                mLegacyScreenCompatPackages.delete(ai.packageName.hashCode());
+            }
+        }
+        return info;
     }
 
     float getCompatScale(String packageName, int uid) {
@@ -718,14 +742,23 @@
 
             scheduleWrite();
 
-            final Task rootTask = mService.getTopDisplayFocusedRootTask();
-            ActivityRecord starting = rootTask.restartPackage(packageName);
-
+            final ArrayList<WindowProcessController> restartedApps = new ArrayList<>();
+            mService.mRootWindowContainer.forAllWindows(w -> {
+                final ActivityRecord ar = w.mActivityRecord;
+                if (ar != null) {
+                    if (ar.packageName.equals(packageName) && !restartedApps.contains(ar.app)) {
+                        ar.restartProcessIfVisible();
+                        restartedApps.add(ar.app);
+                    }
+                } else if (w.getProcess().mInfo.packageName.equals(packageName)) {
+                    w.updateGlobalScale();
+                }
+            }, true /* traverseTopToBottom */);
             // Tell all processes that loaded this package about the change.
             SparseArray<WindowProcessController> pidMap = mService.mProcessMap.getPidMap();
             for (int i = pidMap.size() - 1; i >= 0; i--) {
                 final WindowProcessController app = pidMap.valueAt(i);
-                if (!app.containsPackage(packageName)) {
+                if (!app.containsPackage(packageName) || restartedApps.contains(app)) {
                     continue;
                 }
                 try {
@@ -737,14 +770,6 @@
                 } catch (Exception e) {
                 }
             }
-
-            if (starting != null) {
-                starting.ensureActivityConfiguration(0 /* globalChanges */,
-                        false /* preserveWindow */);
-                // And we need to make sure at this point that all other activities
-                // are made visible with the correct configuration.
-                rootTask.ensureActivitiesVisible(starting, 0, !PRESERVE_WINDOWS);
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
index e91857f..735c73a 100644
--- a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
+++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
@@ -171,12 +171,14 @@
 
         mLocalAnimationAdapter.startAnimation(dim.mDimSurface, t,
                 ANIMATION_TYPE_DIMMER, /* finishCallback */ (type, animator) -> {
-                    setAlphaBlur(dim.mDimSurface, targetAlpha, targetBlur, t);
-                    if (targetAlpha == 0f && !dim.isDimming()) {
-                        dim.remove(t);
+                    synchronized (dim.mHostContainer.mWmService.mGlobalLock) {
+                        setAlphaBlur(dim.mDimSurface, targetAlpha, targetBlur, t);
+                        if (targetAlpha == 0f && !dim.isDimming()) {
+                            dim.remove(t);
+                        }
+                        mLocalAnimationAdapter = null;
+                        mAlphaAnimationSpec = null;
                     }
-                    mLocalAnimationAdapter = null;
-                    mAlphaAnimationSpec = null;
                 });
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ae10ce3..f8dc9c7 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -512,7 +512,11 @@
      */
     private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics();
 
-    /** The desired scaling factor for compatible apps. */
+    /**
+     * The desired scaling factor for compatible apps. It limits the size of the window to be
+     * original size ([320x480] x density). Used to scale window for applications running under
+     * legacy compatibility mode.
+     */
     float mCompatibleScreenScale;
 
     /** @see #getCurrentOverrideConfigurationChanges */
@@ -4794,7 +4798,8 @@
             assignRelativeLayerForIme(getSyncTransaction(), true /* forceUpdate */);
             scheduleAnimation();
 
-            mWmService.mH.post(() -> InputMethodManagerInternal.get().onImeParentChanged());
+            mWmService.mH.post(
+                    () -> InputMethodManagerInternal.get().onImeParentChanged(getDisplayId()));
         } 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
@@ -7090,7 +7095,7 @@
         }
 
         @Override
-        public void notifyInsetsControlChanged() {
+        public void notifyInsetsControlChanged(int displayId) {
             final InsetsStateController stateController = getInsetsStateController();
             try {
                 mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(),
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index 8ecbc17..b74eb56 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -29,8 +29,10 @@
 
     /**
      * Notifies the control target that the insets control has changed.
+     *
+     * @param displayId the display hosting the window of this target
      */
-    default void notifyInsetsControlChanged() {
+    default void notifyInsetsControlChanged(int displayId) {
     };
 
     /**
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 7815679..3c556bf 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -728,7 +728,7 @@
         }
 
         @Override
-        public void notifyInsetsControlChanged() {
+        public void notifyInsetsControlChanged(int displayId) {
             mHandler.post(this);
         }
 
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index c4d0129..6b9fcf4 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -72,7 +72,7 @@
     };
     private final InsetsControlTarget mEmptyImeControlTarget = new InsetsControlTarget() {
         @Override
-        public void notifyInsetsControlChanged() {
+        public void notifyInsetsControlChanged(int displayId) {
             InsetsSourceControl[] controls = getControlsForDispatch(this);
             if (controls == null) {
                 return;
@@ -80,7 +80,7 @@
             for (InsetsSourceControl control : controls) {
                 if (control.getType() == WindowInsets.Type.ime()) {
                     mDisplayContent.mWmService.mH.post(() ->
-                            InputMethodManagerInternal.get().removeImeSurface());
+                            InputMethodManagerInternal.get().removeImeSurface(displayId));
                 }
             }
         }
@@ -370,9 +370,10 @@
                 provider.onSurfaceTransactionApplied();
             }
             final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
+            int displayId = mDisplayContent.getDisplayId();
             for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) {
                 final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i);
-                controlTarget.notifyInsetsControlChanged();
+                controlTarget.notifyInsetsControlChanged(displayId);
                 if (mControlTargetProvidersMap.containsKey(controlTarget)) {
                     // We only collect targets who get controls, not lose controls.
                     newControlTargets.add(controlTarget);
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 5518de7..9305396 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -20,6 +20,7 @@
 import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
 import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
@@ -170,6 +171,8 @@
 
     // Corresponds to OVERRIDE_ANY_ORIENTATION
     private final boolean mIsOverrideAnyOrientationEnabled;
+    // Corresponds to OVERRIDE_ANY_ORIENTATION_TO_USER
+    private final boolean mIsOverrideToUserOrientationEnabled;
     // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
     private final boolean mIsOverrideToPortraitOrientationEnabled;
     // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR
@@ -254,9 +257,8 @@
     // Counter for ActivityRecord#setRequestedOrientation
     private int mSetOrientationRequestCounter = 0;
 
-    // The min aspect ratio override set by user. Stores the last selected aspect ratio after
-    // {@link #shouldApplyUserFullscreenOverride} or {@link #shouldApplyUserMinAspectRatioOverride}
-    // have been invoked.
+    // TODO(b/315140179): Make mUserAspectRatio final
+    // The min aspect ratio override set by user
     @PackageManager.UserMinAspectRatio
     private int mUserAspectRatio = USER_MIN_ASPECT_RATIO_UNSET;
 
@@ -355,6 +357,8 @@
                         PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
 
         mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION);
+        mIsOverrideToUserOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION_TO_USER);
         mIsOverrideToPortraitOrientationEnabled =
                 isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT);
         mIsOverrideToReverseLandscapeOrientationEnabled =
@@ -663,9 +667,11 @@
 
     @ScreenOrientation
     int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
+        final DisplayContent displayContent = mActivityRecord.mDisplayContent;
+        final boolean isIgnoreOrientationRequestEnabled = displayContent != null
+                && displayContent.getIgnoreOrientationRequest();
         if (shouldApplyUserFullscreenOverride()
-                && mActivityRecord.mDisplayContent != null
-                && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()) {
+                && isIgnoreOrientationRequestEnabled) {
             Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
                     + mActivityRecord + " is overridden to "
                     + screenOrientationToString(SCREEN_ORIENTATION_USER)
@@ -690,7 +696,6 @@
             return candidate;
         }
 
-        DisplayContent displayContent = mActivityRecord.mDisplayContent;
         if (mIsOverrideOrientationOnlyForCameraEnabled && displayContent != null
                 && (displayContent.mDisplayRotationCompatPolicy == null
                         || !displayContent.mDisplayRotationCompatPolicy
@@ -698,6 +703,17 @@
             return candidate;
         }
 
+        // mUserAspectRatio is always initialized first in shouldApplyUserFullscreenOverride(),
+        // which will always come first before this check as user override > device
+        // manufacturer override.
+        if (mUserAspectRatio == PackageManager.USER_MIN_ASPECT_RATIO_UNSET
+                && mIsOverrideToUserOrientationEnabled && isIgnoreOrientationRequestEnabled) {
+            Slog.v(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
+                    + mActivityRecord + " is overridden to "
+                    + screenOrientationToString(SCREEN_ORIENTATION_USER));
+            return SCREEN_ORIENTATION_USER;
+        }
+
         if (mIsOverrideToReverseLandscapeOrientationEnabled
                 && (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
             Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 57939bc..7995028 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -89,6 +89,7 @@
 import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.BiConsumer;
@@ -106,7 +107,7 @@
     final WindowProcessController mProcess;
     private final String mStringName;
     SurfaceSession mSurfaceSession;
-    private int mNumWindow = 0;
+    private final ArrayList<WindowState> mAddedWindows = new ArrayList<>();
     // Set of visible application overlay window surfaces connected to this session.
     private final ArraySet<WindowSurfaceController> mAppOverlaySurfaces = new ArraySet<>();
     // Set of visible alert window surfaces connected to this session.
@@ -192,8 +193,7 @@
         try {
             mCallback.asBinder().linkToDeath(this, 0);
         } catch (RemoteException e) {
-            // The caller has died, so we can just forget about this.
-            // Hmmm, should we call killSessionLocked()??
+            mClientDead = true;
         }
     }
 
@@ -211,12 +211,27 @@
         }
     }
 
+    boolean isClientDead() {
+        return mClientDead;
+    }
+
     @Override
     public void binderDied() {
         synchronized (mService.mGlobalLock) {
             mCallback.asBinder().unlinkToDeath(this, 0);
             mClientDead = true;
-            killSessionLocked();
+            try {
+                for (int i = mAddedWindows.size() - 1; i >= 0; i--) {
+                    final WindowState w = mAddedWindows.get(i);
+                    Slog.i(TAG_WM, "WIN DEATH: " + w);
+                    if (w.mActivityRecord != null && w.mActivityRecord.findMainWindow() == w) {
+                        mService.mSnapshotController.onAppDied(w.mActivityRecord);
+                    }
+                    w.removeIfPossible();
+                }
+            } finally {
+                killSessionLocked();
+            }
         }
     }
 
@@ -741,7 +756,7 @@
         }
     }
 
-    void windowAddedLocked() {
+    void onWindowAdded(WindowState w) {
         if (mPackageName == null) {
             mPackageName = mProcess.mInfo.packageName;
             mRelayoutTag = "relayoutWindow: " + mPackageName;
@@ -757,12 +772,14 @@
                 mService.dispatchNewAnimatorScaleLocked(this);
             }
         }
-        mNumWindow++;
+        mAddedWindows.add(w);
     }
 
-    void windowRemovedLocked() {
-        mNumWindow--;
-        killSessionLocked();
+    void onWindowRemoved(WindowState w) {
+        mAddedWindows.remove(w);
+        if (mAddedWindows.isEmpty()) {
+            killSessionLocked();
+        }
     }
 
 
@@ -829,7 +846,7 @@
     }
 
     private void killSessionLocked() {
-        if (mNumWindow > 0 || !mClientDead) {
+        if (!mClientDead) {
             return;
         }
 
@@ -838,10 +855,6 @@
             return;
         }
 
-        if (DEBUG) {
-            Slog.v(TAG_WM, "Last window removed from " + this
-                    + ", destroying " + mSurfaceSession);
-        }
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "  KILL SURFACE SESSION %s", mSurfaceSession);
         try {
             mSurfaceSession.kill();
@@ -850,6 +863,7 @@
                     + " in session " + this + ": " + e.toString());
         }
         mSurfaceSession = null;
+        mAddedWindows.clear();
         mAlertWindowSurfaces.clear();
         mAppOverlaySurfaces.clear();
         setHasOverlayUi(false);
@@ -869,7 +883,7 @@
     }
 
     void dump(PrintWriter pw, String prefix) {
-        pw.print(prefix); pw.print("mNumWindow="); pw.print(mNumWindow);
+        pw.print(prefix); pw.print("numWindow="); pw.print(mAddedWindows.size());
                 pw.print(" mCanAddInternalSystemWindow="); pw.print(mCanAddInternalSystemWindow);
                 pw.print(" mAppOverlaySurfaces="); pw.print(mAppOverlaySurfaces);
                 pw.print(" mAlertWindowSurfaces="); pw.print(mAlertWindowSurfaces);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 671acfc..dbfcc22 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -34,7 +34,6 @@
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
-import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
@@ -5917,22 +5916,6 @@
         return activities;
     }
 
-    ActivityRecord restartPackage(String packageName) {
-        ActivityRecord starting = topRunningActivity();
-
-        // All activities that came from the package must be
-        // restarted as if there was a config change.
-        forAllActivities(r -> {
-            if (!r.info.packageName.equals(packageName)) return;
-            r.forceNewConfig = true;
-            if (starting != null && r == starting && r.isVisibleRequested()) {
-                r.startFreezingScreenLocked(CONFIG_SCREEN_LAYOUT);
-            }
-        });
-
-        return starting;
-    }
-
     Task reuseOrCreateTask(ActivityInfo info, Intent intent, boolean toTop) {
         return reuseOrCreateTask(info, intent, null /*voiceSession*/, null /*voiceInteractor*/,
                 toTop, null /*activity*/, null /*source*/, null /*options*/);
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f020bfa..3117db5 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -43,6 +43,7 @@
 import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
 import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
+import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
 import static android.window.TransitionInfo.FLAG_FILLS_TASK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
@@ -3067,6 +3068,10 @@
                         Slog.e(TAG, "Unexpected launch-task-behind operation in shell transition");
                         flags |= FLAG_TASK_LAUNCHING_BEHIND;
                     }
+                    if ((topActivity.mTransitionChangeFlags & FLAGS_IS_OCCLUDED_NO_ANIMATION)
+                            == FLAGS_IS_OCCLUDED_NO_ANIMATION) {
+                        flags |= FLAGS_IS_OCCLUDED_NO_ANIMATION;
+                    }
                 }
                 if (task.voiceSession != null) {
                     flags |= FLAG_IS_VOICE_INTERACTION;
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index e648d64..c3aca6f 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -731,9 +731,9 @@
             }
 
             // set the pip task in the request if provided
-            if (mCollectingTransition.getPipActivity() != null) {
-                pipTaskInfo = mCollectingTransition.getPipActivity().getTask().getTaskInfo();
-                mCollectingTransition.setPipActivity(null);
+            if (transition.getPipActivity() != null) {
+                pipTaskInfo = transition.getPipActivity().getTask().getTaskInfo();
+                transition.setPipActivity(null);
             }
 
             final TransitionRequestInfo request = new TransitionRequestInfo(transition.mType,
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 2125c63..c1310a6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1445,6 +1445,11 @@
             if (!mDisplayReady) {
                 throw new IllegalStateException("Display has not been initialialized");
             }
+            if (session.isClientDead()) {
+                ProtoLog.w(WM_ERROR, "Attempted to add window with a client %s "
+                        + "that is dead. Aborting.", session);
+                return WindowManagerGlobal.ADD_APP_EXITING;
+            }
 
             final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);
 
@@ -1629,19 +1634,6 @@
             final WindowState win = new WindowState(this, session, client, token, parentWindow,
                     appOp[0], attrs, viewVisibility, session.mUid, userId,
                     session.mCanAddInternalSystemWindow);
-            if (win.mDeathRecipient == null) {
-                // Client has apparently died, so there is no reason to
-                // continue.
-                ProtoLog.w(WM_ERROR, "Adding window client %s"
-                        + " that is dead, aborting.", client.asBinder());
-                return WindowManagerGlobal.ADD_APP_EXITING;
-            }
-
-            if (win.getDisplayContent() == null) {
-                ProtoLog.w(WM_ERROR, "Adding window to Display that has been removed.");
-                return WindowManagerGlobal.ADD_INVALID_DISPLAY;
-            }
-
             final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
             displayPolicy.adjustWindowParamsLw(win, win.mAttrs);
             attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid);
@@ -1725,7 +1717,7 @@
                 displayContent.mTapExcludedWindows.add(win);
             }
 
-            win.attach();
+            win.mSession.onWindowAdded(win);
             mWindowMap.put(client.asBinder(), win);
             win.initAppOpsState();
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4e9d23c..32638e0 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -52,7 +52,6 @@
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NOT_MAGNIFIABLE;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY;
@@ -203,7 +202,6 @@
 import android.os.Debug;
 import android.os.IBinder;
 import android.os.PowerManager;
-import android.os.PowerManager.WakeReason;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -310,7 +308,6 @@
     // mAttrs.flags is tested in animation without being locked. If the bits tested are ever
     // modified they will need to be locked.
     final WindowManager.LayoutParams mAttrs = new WindowManager.LayoutParams();
-    final DeathRecipient mDeathRecipient;
     private boolean mIsChildWindow;
     final int mBaseLayer;
     final int mSubLayer;
@@ -704,11 +701,6 @@
      */
     private final Region mTapExcludeRegion = new Region();
 
-    /**
-     * Used for testing because the real PowerManager is final.
-     */
-    private PowerManagerWrapper mPowerManagerWrapper;
-
     private static final StringBuilder sTmpSB = new StringBuilder();
 
     /**
@@ -1063,34 +1055,9 @@
         return mOnBackInvokedCallbackInfo;
     }
 
-    interface PowerManagerWrapper {
-        void wakeUp(long time, @WakeReason int reason, String details);
-
-        boolean isInteractive();
-
-    }
-
     WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
             WindowState parentWindow, int appOp, WindowManager.LayoutParams a, int viewVisibility,
             int ownerId, int showUserId, boolean ownerCanAddInternalSystemWindow) {
-        this(service, s, c, token, parentWindow, appOp, a, viewVisibility, ownerId, showUserId,
-                ownerCanAddInternalSystemWindow, new PowerManagerWrapper() {
-                    @Override
-                    public void wakeUp(long time, @WakeReason int reason, String details) {
-                        service.mPowerManager.wakeUp(time, reason, details);
-                    }
-
-                    @Override
-                    public boolean isInteractive() {
-                        return service.mPowerManager.isInteractive();
-                    }
-                });
-    }
-
-    WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
-            WindowState parentWindow, int appOp, WindowManager.LayoutParams a, int viewVisibility,
-            int ownerId, int showUserId, boolean ownerCanAddInternalSystemWindow,
-            PowerManagerWrapper powerManagerWrapper) {
         super(service);
         mTmpTransaction = service.mTransactionFactory.get();
         mSession = s;
@@ -1108,8 +1075,6 @@
         mViewVisibility = viewVisibility;
         mPolicy = mWmService.mPolicy;
         mContext = mWmService.mContext;
-        DeathRecipient deathRecipient = new DeathRecipient();
-        mPowerManagerWrapper = powerManagerWrapper;
         mForceSeamlesslyRotate = token.mRoundedCornerOverlay;
         mInputWindowHandle = new InputWindowHandleWrapper(new InputWindowHandle(
                 mActivityRecord != null
@@ -1128,22 +1093,6 @@
             Slog.v(TAG, "Window " + this + " client=" + c.asBinder()
                             + " token=" + token + " (" + mAttrs.token + ")" + " params=" + a);
         }
-        try {
-            c.asBinder().linkToDeath(deathRecipient, 0);
-        } catch (RemoteException e) {
-            mDeathRecipient = null;
-            mIsChildWindow = false;
-            mLayoutAttached = false;
-            mIsImWindow = false;
-            mIsWallpaper = false;
-            mIsFloatingLayer = false;
-            mBaseLayer = 0;
-            mSubLayer = 0;
-            mWinAnimator = null;
-            mOverrideScale = 1f;
-            return;
-        }
-        mDeathRecipient = deathRecipient;
 
         if (mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW) {
             // The multiplier here is to reserve space for multiple
@@ -1238,11 +1187,6 @@
         return TouchOcclusionMode.BLOCK_UNTRUSTED;
     }
 
-    void attach() {
-        if (DEBUG) Slog.v(TAG, "Attaching " + this + " token=" + mToken);
-        mSession.windowAddedLocked();
-    }
-
     void updateGlobalScale() {
         if (hasCompatScale()) {
             mCompatScale = (mOverrideScale == 1f || mToken.hasSizeCompatBounds())
@@ -1270,13 +1214,14 @@
      * @see ActivityRecord#hasSizeCompatBounds()
      */
     boolean hasCompatScale() {
-        if ((mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) {
-            return true;
-        }
         if (mAttrs.type == TYPE_APPLICATION_STARTING) {
             // Exclude starting window because it is not displayed by the application.
             return false;
         }
+        if (mWmService.mAtmService.mCompatModePackages.useLegacyScreenCompatMode(
+                mSession.mProcess.mInfo.packageName)) {
+            return true;
+        }
         return mActivityRecord != null && mActivityRecord.hasSizeCompatBounds()
                 || mOverrideScale != 1f;
     }
@@ -2398,14 +2343,7 @@
         disposeInputChannel();
         mOnBackInvokedCallbackInfo = null;
 
-        mSession.windowRemovedLocked();
-        try {
-            mClient.asBinder().unlinkToDeath(mDeathRecipient, 0);
-        } catch (RuntimeException e) {
-            // Ignore if it has already been removed (usually because
-            // we are doing this as part of processing a death note.)
-        }
-
+        mSession.onWindowRemoved(this);
         mWmService.postWindowRemoveCleanupLocked(this);
 
         mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens(
@@ -2861,12 +2799,12 @@
             boolean canTurnScreenOn = mActivityRecord == null || mActivityRecord.currentLaunchCanTurnScreenOn();
 
             if (allowTheaterMode && canTurnScreenOn
-                        && (mWmService.mAtmService.isDreaming()
-                        || !mPowerManagerWrapper.isInteractive())) {
+                    && (mWmService.mAtmService.isDreaming()
+                            || !mWmService.mPowerManager.isInteractive())) {
                 if (DEBUG_VISIBILITY || DEBUG_POWER) {
                     Slog.v(TAG, "Relayout window turning screen on: " + this);
                 }
-                mPowerManagerWrapper.wakeUp(SystemClock.uptimeMillis(),
+                mWmService.mPowerManager.wakeUp(SystemClock.uptimeMillis(),
                         PowerManager.WAKE_REASON_APPLICATION, "android.server.wm:SCREEN_ON_FLAG");
             }
 
@@ -2935,31 +2873,6 @@
         }
     }
 
-    private class DeathRecipient implements IBinder.DeathRecipient {
-        @Override
-        public void binderDied() {
-            try {
-                synchronized (mWmService.mGlobalLock) {
-                    final WindowState win = mWmService
-                            .windowForClientLocked(mSession, mClient, false);
-                    Slog.i(TAG, "WIN DEATH: " + win);
-                    if (win != null) {
-                        if (win.mActivityRecord != null
-                                && win.mActivityRecord.findMainWindow() == win) {
-                            mWmService.mSnapshotController.onAppDied(win.mActivityRecord);
-                        }
-                        win.removeIfPossible();
-                    } else if (mHasSurface) {
-                        Slog.e(TAG, "!!! LEAK !!! Window removed but surface still valid.");
-                        WindowState.this.removeIfPossible();
-                    }
-                }
-            } catch (IllegalArgumentException ex) {
-                // This will happen if the window has already been removed.
-            }
-        }
-    }
-
     /** Returns {@code true} if this window desires key events. */
     boolean canReceiveKeys() {
         return canReceiveKeys(false /* fromUserTouch */);
@@ -3830,7 +3743,7 @@
     }
 
     @Override
-    public void notifyInsetsControlChanged() {
+    public void notifyInsetsControlChanged(int displayId) {
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "notifyInsetsControlChanged for %s ", this);
         if (mRemoved) {
             return;
@@ -5724,6 +5637,12 @@
             // Skip sync for invisible app windows which are not managed by activity lifecycle.
             return false;
         }
+        if (mActivityRecord != null && mViewVisibility != View.VISIBLE
+                && mWinAnimator.mAttrType != TYPE_BASE_APPLICATION
+                && mWinAnimator.mAttrType != TYPE_APPLICATION_STARTING) {
+            // Skip sync for invisible app windows which are not managed by activity lifecycle.
+            return false;
+        }
         // In the WindowContainer implementation we immediately mark ready
         // since a generic WindowContainer only needs to wait for its
         // children to finish and is immediately ready from its own
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index c34e701..b19f3d8 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -37,7 +37,6 @@
         "com_android_server_adb_AdbDebuggingManager.cpp",
         "com_android_server_am_BatteryStatsService.cpp",
         "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
-        "com_android_server_BootReceiver.cpp",
         "com_android_server_ConsumerIrService.cpp",
         "com_android_server_companion_virtual_InputController.cpp",
         "com_android_server_devicepolicy_CryptoTestHelper.cpp",
@@ -94,16 +93,6 @@
     header_libs: [
         "bionic_libc_platform_headers",
     ],
-
-    static_libs: [
-        "libunwindstack",
-    ],
-
-    whole_static_libs: [
-        "libdebuggerd_tombstone_proto_to_text",
-    ],
-
-    runtime_libs: ["libdexfile"],
 }
 
 cc_defaults {
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 0d762dfb..cc08488 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -31,6 +31,5 @@
 per-file com_android_server_am_CachedAppOptimizer.cpp = timmurray@google.com, edgararriaga@google.com, dualli@google.com, carmenjackson@google.com, philipcuadra@google.com
 per-file com_android_server_companion_virtual_InputController.cpp = file:/services/companion/java/com/android/server/companion/virtual/OWNERS
 
-# Bug component : 158088 = per-file com_android_server_utils_AnrTimer*.java
-per-file com_android_server_utils_AnrTimer*.java = file:/PERFORMANCE_OWNERS
-per-file com_android_server_BootReceiver.cpp = file:/STABILITY_OWNERS
+# Bug component : 158088 = per-file *AnrTimer*
+per-file *AnrTimer* = file:/PERFORMANCE_OWNERS
diff --git a/services/core/jni/com_android_server_BootReceiver.cpp b/services/core/jni/com_android_server_BootReceiver.cpp
deleted file mode 100644
index 3892d28..0000000
--- a/services/core/jni/com_android_server_BootReceiver.cpp
+++ /dev/null
@@ -1,57 +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.
- */
-
-#include <libdebuggerd/tombstone.h>
-#include <nativehelper/JNIHelp.h>
-
-#include <sstream>
-
-#include "jni.h"
-#include "tombstone.pb.h"
-
-namespace android {
-
-static void writeToString(std::stringstream& ss, const std::string& line, bool should_log) {
-    ss << line << std::endl;
-}
-
-static jstring com_android_server_BootReceiver_getTombstoneText(JNIEnv* env, jobject,
-                                                                jbyteArray tombstoneBytes) {
-    Tombstone tombstone;
-    tombstone.ParseFromArray(env->GetByteArrayElements(tombstoneBytes, 0),
-                             env->GetArrayLength(tombstoneBytes));
-
-    std::stringstream tombstoneString;
-
-    tombstone_proto_to_text(tombstone,
-                            std::bind(&writeToString, std::ref(tombstoneString),
-                                      std::placeholders::_1, std::placeholders::_2));
-
-    return env->NewStringUTF(tombstoneString.str().c_str());
-}
-
-static const JNINativeMethod sMethods[] = {
-        /* name, signature, funcPtr */
-        {"getTombstoneText", "([B)Ljava/lang/String;",
-         (jstring*)com_android_server_BootReceiver_getTombstoneText},
-};
-
-int register_com_android_server_BootReceiver(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "com/android/server/BootReceiver", sMethods,
-                                    NELEM(sMethods));
-}
-
-} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index deae82c..11734da 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -65,7 +65,6 @@
 int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env);
 int register_android_server_companion_virtual_InputController(JNIEnv* env);
 int register_android_server_app_GameManagerService(JNIEnv* env);
-int register_com_android_server_BootReceiver(JNIEnv* env);
 int register_com_android_server_wm_TaskFpsCallbackController(JNIEnv* env);
 int register_com_android_server_display_DisplayControl(JNIEnv* env);
 int register_com_android_server_SystemClockTime(JNIEnv* env);
@@ -127,7 +126,6 @@
     register_android_server_sensor_SensorService(vm, env);
     register_android_server_companion_virtual_InputController(env);
     register_android_server_app_GameManagerService(env);
-    register_com_android_server_BootReceiver(env);
     register_com_android_server_wm_TaskFpsCallbackController(env);
     register_com_android_server_display_DisplayControl(env);
     register_com_android_server_SystemClockTime(env);
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index c625b1e..3cbceec 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -595,7 +595,7 @@
             <!-- Sets the brightness mapping of the desired screen brightness to the corresponding
             lux for the current display -->
             <xs:element name="luxToBrightnessMapping" type="luxToBrightnessMapping"
-                        minOccurs="0" maxOccurs="1">
+                        minOccurs="0" maxOccurs="unbounded">
                 <xs:annotation name="final"/>
             </xs:element>
         </xs:sequence>
@@ -619,12 +619,20 @@
 
     This is used in place of config_autoBrightnessLevels and config_autoBrightnessLcdBacklightValues
     defined in the config XML resource.
+
+    On devices that allow users to choose from a set of predefined options in display
+    auto-brightness settings, multiple mappings for different modes and settings can be defined.
+
+    If no mode is specified, the mapping will be used for the default mode.
+    If no setting is specified, the mapping will be used for the normal brightness setting.
     -->
     <xs:complexType name="luxToBrightnessMapping">
         <xs:element name="map" type="nonNegativeFloatToFloatMap">
             <xs:annotation name="nonnull"/>
             <xs:annotation name="final"/>
         </xs:element>
+        <xs:element name="mode" type="AutoBrightnessModeName" minOccurs="0"/>
+        <xs:element name="setting" type="AutoBrightnessSettingName" minOccurs="0"/>
     </xs:complexType>
 
     <!-- Represents a point in the display brightness mapping, representing the lux level from the
@@ -757,4 +765,23 @@
             </xs:element>
         </xs:sequence>
     </xs:complexType>
+
+    <!-- Predefined type names as defined by
+    AutomaticBrightnessController.AutomaticBrightnessMode -->
+    <xs:simpleType  name="AutoBrightnessModeName">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="default"/>
+            <xs:enumeration value="idle"/>
+            <xs:enumeration value="doze"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <!-- Predefined auto-brighntess settings -->
+    <xs:simpleType  name="AutoBrightnessSettingName">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="dim"/>
+            <xs:enumeration value="normal"/>
+            <xs:enumeration value="bright"/>
+        </xs:restriction>
+    </xs:simpleType>
 </xs:schema>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 8c8c123..79ea274 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -8,13 +8,26 @@
     method public final java.math.BigInteger getDarkeningLightDebounceIdleMillis();
     method public final java.math.BigInteger getDarkeningLightDebounceMillis();
     method public boolean getEnabled();
-    method public final com.android.server.display.config.LuxToBrightnessMapping getLuxToBrightnessMapping();
+    method public final java.util.List<com.android.server.display.config.LuxToBrightnessMapping> getLuxToBrightnessMapping();
     method public final void setBrighteningLightDebounceIdleMillis(java.math.BigInteger);
     method public final void setBrighteningLightDebounceMillis(java.math.BigInteger);
     method public final void setDarkeningLightDebounceIdleMillis(java.math.BigInteger);
     method public final void setDarkeningLightDebounceMillis(java.math.BigInteger);
     method public void setEnabled(boolean);
-    method public final void setLuxToBrightnessMapping(com.android.server.display.config.LuxToBrightnessMapping);
+  }
+
+  public enum AutoBrightnessModeName {
+    method public String getRawName();
+    enum_constant public static final com.android.server.display.config.AutoBrightnessModeName _default;
+    enum_constant public static final com.android.server.display.config.AutoBrightnessModeName doze;
+    enum_constant public static final com.android.server.display.config.AutoBrightnessModeName idle;
+  }
+
+  public enum AutoBrightnessSettingName {
+    method public String getRawName();
+    enum_constant public static final com.android.server.display.config.AutoBrightnessSettingName bright;
+    enum_constant public static final com.android.server.display.config.AutoBrightnessSettingName dim;
+    enum_constant public static final com.android.server.display.config.AutoBrightnessSettingName normal;
   }
 
   public class BlockingZoneConfig {
@@ -220,7 +233,11 @@
   public class LuxToBrightnessMapping {
     ctor public LuxToBrightnessMapping();
     method @NonNull public final com.android.server.display.config.NonNegativeFloatToFloatMap getMap();
+    method public com.android.server.display.config.AutoBrightnessModeName getMode();
+    method public com.android.server.display.config.AutoBrightnessSettingName getSetting();
     method public final void setMap(@NonNull com.android.server.display.config.NonNegativeFloatToFloatMap);
+    method public void setMode(com.android.server.display.config.AutoBrightnessModeName);
+    method public void setSetting(com.android.server.display.config.AutoBrightnessSettingName);
   }
 
   public class NitsMap {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e0a2f30..a490013 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -2469,7 +2469,7 @@
     private void migratePersonalAppSuspensionLocked(
             int doUserId, int poUserId, ActiveAdmin poAdmin) {
         final PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
-        if (!pmi.isSuspendingAnyPackages(PLATFORM_PACKAGE_NAME, doUserId)) {
+        if (!pmi.isAdminSuspendingAnyPackages(doUserId)) {
             Slogf.i(LOG_TAG, "DO is not suspending any apps.");
             return;
         }
@@ -2480,7 +2480,7 @@
             poAdmin.mSuspendPersonalApps = true;
         } else {
             Slogf.i(LOG_TAG, "PO isn't targeting R+, unsuspending personal apps.");
-            pmi.unsuspendForSuspendingPackage(PLATFORM_PACKAGE_NAME, doUserId);
+            pmi.unsuspendAdminSuspendedPackages(doUserId);
         }
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index 6570ce1..506dbe8 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -16,8 +16,6 @@
 
 package com.android.server.devicepolicy;
 
-import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppGlobals;
@@ -287,7 +285,7 @@
                 suspendPersonalAppsInPackageManager(context, userId);
             } else {
                 LocalServices.getService(PackageManagerInternal.class)
-                        .unsuspendForSuspendingPackage(PLATFORM_PACKAGE_NAME, userId);
+                        .unsuspendAdminSuspendedPackages(userId);
             }
         });
         return true;
diff --git a/services/manifest_services.xml b/services/manifest_services.xml
index eae159f..8ff1a7d 100644
--- a/services/manifest_services.xml
+++ b/services/manifest_services.xml
@@ -1,7 +1,7 @@
 <manifest version="1.0" type="framework">
     <hal format="aidl">
         <name>android.frameworks.location.altitude</name>
-        <version>1</version>
+        <version>2</version>
         <fqname>IAltitudeService/default</fqname>
     </hal>
     <hal format="aidl">
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index f69f628..022268d 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1262,7 +1262,7 @@
         val apexModuleName = packageState.apexModuleName
         val packageName = packageState.packageName
         return when {
-            packageState.isVendor ->
+            packageState.isVendor || packageState.isOdm ->
                 permissionAllowlist.getVendorPrivilegedAppAllowlistState(
                     packageName,
                     permissionName
@@ -1471,12 +1471,15 @@
                     // In any case, don't grant a privileged permission to privileged vendor apps,
                     // if the permission's protectionLevel does not have the extra vendorPrivileged
                     // flag.
-                    if (packageState.isVendor && !permission.isVendorPrivileged) {
+                    if (
+                        (packageState.isVendor || packageState.isOdm) &&
+                            !permission.isVendorPrivileged
+                    ) {
                         Slog.w(
                             LOG_TAG,
                             "Permission $permissionName cannot be granted to privileged" +
-                                " vendor app $packageName because it isn't a vendorPrivileged" +
-                                " permission"
+                                " vendor (or odm) app $packageName because it isn't a" +
+                                " vendorPrivileged permission"
                         )
                         return false
                     }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 3d0b389..6fff012 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -36,6 +36,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -50,6 +51,7 @@
 import android.content.pm.SharedLibraryInfo;
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.os.BaseBundle;
 import android.os.Message;
 import android.os.PersistableBundle;
@@ -57,6 +59,9 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
@@ -122,6 +127,8 @@
     private static final String PACKAGE_NAME_3 = "com.android.app3";
     private static final int TEST_RESOURCE_ID = 2131231283;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
     @Mock
     RuntimePermissionsPersistence mRuntimePermissionsPersistence;
     @Mock
@@ -425,7 +432,7 @@
         PackageUserStateInternal packageUserState1 = ps1.readUserState(0);
         assertThat(packageUserState1.isSuspended(), is(true));
         assertThat(packageUserState1.getSuspendParams().size(), is(1));
-        assertThat(packageUserState1.getSuspendParams().keyAt(0), is("android"));
+        assertThat(packageUserState1.getSuspendParams().keyAt(0), is(UserPackage.of(0, "android")));
         assertThat(packageUserState1.getSuspendParams().valueAt(0).getAppExtras(), is(nullValue()));
         assertThat(packageUserState1.getSuspendParams().valueAt(0).getDialogInfo(),
                 is(nullValue()));
@@ -437,7 +444,7 @@
         packageUserState1 = ps1.readUserState(0);
         assertThat(packageUserState1.isSuspended(), is(true));
         assertThat(packageUserState1.getSuspendParams().size(), is(1));
-        assertThat(packageUserState1.getSuspendParams().keyAt(0), is("android"));
+        assertThat(packageUserState1.getSuspendParams().keyAt(0), is(UserPackage.of(0, "android")));
         assertThat(packageUserState1.getSuspendParams().valueAt(0).getAppExtras(), is(nullValue()));
         assertThat(packageUserState1.getSuspendParams().valueAt(0).getDialogInfo(),
                 is(nullValue()));
@@ -472,7 +479,8 @@
         watcher.verifyNoChangeReported("readUserState");
         assertThat(packageUserState1.isSuspended(), is(true));
         assertThat(packageUserState1.getSuspendParams().size(), is(1));
-        assertThat(packageUserState1.getSuspendParams().keyAt(0), is(PACKAGE_NAME_3));
+        assertThat(packageUserState1.getSuspendParams().keyAt(0),
+                is(UserPackage.of(0, PACKAGE_NAME_3)));
         final SuspendParams params = packageUserState1.getSuspendParams().valueAt(0);
         watcher.verifyNoChangeReported("fetch user state");
         assertThat(params, is(notNullValue()));
@@ -523,19 +531,24 @@
                 .setNeutralButtonAction(BUTTON_ACTION_UNSUSPEND)
                 .build();
 
-        ps1.modifyUserState(0).putSuspendParams("suspendingPackage1",
+        UserPackage suspender1 = UserPackage.of(0, "suspendingPackage1");
+        UserPackage suspender2 = UserPackage.of(0, "suspendingPackage2");
+        UserPackage suspender3 = UserPackage.of(0, "suspendingPackage3");
+        UserPackage irrelevantSuspender = UserPackage.of(0, "irrelevant");
+
+        ps1.modifyUserState(0).putSuspendParams(suspender1,
                 new SuspendParams(dialogInfo1, appExtras1, launcherExtras1));
-        ps1.modifyUserState(0).putSuspendParams("suspendingPackage2",
+        ps1.modifyUserState(0).putSuspendParams(suspender2,
                 new SuspendParams(dialogInfo2, appExtras2, launcherExtras2));
         settingsUnderTest.mPackages.put(PACKAGE_NAME_1, ps1);
         watcher.verifyChangeReported("put package 1");
 
-        ps2.modifyUserState(0).putSuspendParams("suspendingPackage3",
+        ps2.modifyUserState(0).putSuspendParams(suspender3,
                 new SuspendParams(null, appExtras1, null));
         settingsUnderTest.mPackages.put(PACKAGE_NAME_2, ps2);
         watcher.verifyChangeReported("put package 2");
 
-        ps3.modifyUserState(0).removeSuspension("irrelevant");
+        ps3.modifyUserState(0).removeSuspension(irrelevantSuspender);
         settingsUnderTest.mPackages.put(PACKAGE_NAME_3, ps3);
         watcher.verifyChangeReported("put package 3");
 
@@ -560,7 +573,7 @@
         assertThat(readPus1.getSuspendParams().size(), is(2));
         watcher.verifyNoChangeReported("read package param");
 
-        assertThat(readPus1.getSuspendParams().keyAt(0), is("suspendingPackage1"));
+        assertThat(readPus1.getSuspendParams().keyAt(0), is(suspender1));
         final SuspendParams params11 = readPus1.getSuspendParams().valueAt(0);
         watcher.verifyNoChangeReported("read package param");
         assertThat(params11, is(notNullValue()));
@@ -570,7 +583,7 @@
                 is(true));
         watcher.verifyNoChangeReported("read package param");
 
-        assertThat(readPus1.getSuspendParams().keyAt(1), is("suspendingPackage2"));
+        assertThat(readPus1.getSuspendParams().keyAt(1), is(suspender2));
         final SuspendParams params12 = readPus1.getSuspendParams().valueAt(1);
         assertThat(params12, is(notNullValue()));
         assertThat(params12.getDialogInfo(), is(dialogInfo2));
@@ -583,7 +596,7 @@
                 .readUserState(0);
         assertThat(readPus2.isSuspended(), is(true));
         assertThat(readPus2.getSuspendParams().size(), is(1));
-        assertThat(readPus2.getSuspendParams().keyAt(0), is("suspendingPackage3"));
+        assertThat(readPus2.getSuspendParams().keyAt(0), is(suspender3));
         final SuspendParams params21 = readPus2.getSuspendParams().valueAt(0);
         assertThat(params21, is(notNullValue()));
         assertThat(params21.getDialogInfo(), is(nullValue()));
@@ -1087,6 +1100,8 @@
             new File(InstrumentationRegistry.getContext().getFilesDir(), "com.android.bar-1");
     private static final File UPDATED_CODE_PATH =
             new File(InstrumentationRegistry.getContext().getFilesDir(), "com.android.bar-2");
+    private static final File UPDATED_CODE_PATH2 =
+            new File(InstrumentationRegistry.getContext().getFilesDir(), "com.android.bar-3");
     private static final long INITIAL_VERSION_CODE = 10023L;
     private static final long UPDATED_VERSION_CODE = 10025L;
 
@@ -1134,7 +1149,8 @@
                 .setNeutralButtonText(0x11220003)
                 .setNeutralButtonAction(BUTTON_ACTION_MORE_DETAILS)
                 .build();
-        origPkgSetting01.modifyUserState(0).putSuspendParams("suspendingPackage1",
+        origPkgSetting01.modifyUserState(0).putSuspendParams(
+                UserPackage.of(0, "suspendingPackage1"),
                 new SuspendParams(dialogInfo1, appExtras1, launcherExtras1));
         origPkgSetting01.setPkg(mockAndroidPackage(origPkgSetting01));
         final PackageSetting testPkgSetting01 = new PackageSetting(
@@ -1182,13 +1198,16 @@
                 null /*mimeGroups*/,
                 UUID.randomUUID(),
                 34 /*targetSdkVersion*/,
-                null /*restrictUpdateHash*/);
+                null /*restrictUpdateHash*/,
+                false /*isDontKill*/);
         assertThat(testPkgSetting01.getPrimaryCpuAbi(), is("arm64-v8a"));
         assertThat(testPkgSetting01.getPrimaryCpuAbiLegacy(), is("arm64-v8a"));
         assertThat(testPkgSetting01.getSecondaryCpuAbi(), is("armeabi"));
         assertThat(testPkgSetting01.getSecondaryCpuAbiLegacy(), is("armeabi"));
         assertThat(testPkgSetting01.getFlags(), is(0));
         assertThat(testPkgSetting01.getPrivateFlags(), is(0));
+        assertThat(testPkgSetting01.getPath(), is(UPDATED_CODE_PATH));
+        assertNull(testPkgSetting01.getOldPaths());
         final PackageUserState userState = testPkgSetting01.readUserState(0);
         verifyUserState(userState, false /*notLaunched*/,
                 false /*stopped*/, false /*installed*/);
@@ -1223,7 +1242,8 @@
                 null /*mimeGroups*/,
                 UUID.randomUUID(),
                 34 /*targetSdkVersion*/,
-                null /*restrictUpdateHash*/);
+                null /*restrictUpdateHash*/,
+                false /*isDontKill*/);
         assertThat(testPkgSetting01.getPrimaryCpuAbi(), is("arm64-v8a"));
         assertThat(testPkgSetting01.getPrimaryCpuAbiLegacy(), is("arm64-v8a"));
         assertThat(testPkgSetting01.getSecondaryCpuAbi(), is("armeabi"));
@@ -1237,6 +1257,74 @@
                 false /*stopped*/, true /*installed*/);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_IMPROVE_INSTALL_DONT_KILL)
+    public void testUpdatePackageSettings02WithOldPaths() throws PackageManagerException {
+        final PackageSetting testPkgSetting01 =
+                createPackageSetting(0 /*sharedUserId*/, 0 /*pkgFlags*/);
+        testPkgSetting01.setInstalled(false /*installed*/, 0 /*userId*/);
+        assertThat(testPkgSetting01.getFlags(), is(0));
+        assertThat(testPkgSetting01.getPrivateFlags(), is(0));
+        final PackageSetting oldPkgSetting01 = new PackageSetting(testPkgSetting01);
+        Settings.updatePackageSetting(
+                testPkgSetting01,
+                null /*disabledPkg*/,
+                null /*existingSharedUserSetting*/,
+                null /*sharedUser*/,
+                UPDATED_CODE_PATH /*codePath*/,
+                null /*legacyNativeLibraryPath*/,
+                "arm64-v8a" /*primaryCpuAbi*/,
+                "armeabi" /*secondaryCpuAbi*/,
+                ApplicationInfo.FLAG_SYSTEM /*pkgFlags*/,
+                ApplicationInfo.PRIVATE_FLAG_PRIVILEGED /*pkgPrivateFlags*/,
+                UserManagerService.getInstance(),
+                null /*usesSdkLibraries*/,
+                null /*usesSdkLibrariesVersions*/,
+                null /*usesSdkLibrariesOptional*/,
+                null /*usesStaticLibraries*/,
+                null /*usesStaticLibrariesVersions*/,
+                null /*mimeGroups*/,
+                UUID.randomUUID(),
+                34 /*targetSdkVersion*/,
+                null /*restrictUpdateHash*/,
+                true /*isDontKill*/);
+        assertThat(testPkgSetting01.getPath(), is(UPDATED_CODE_PATH));
+        assertThat(testPkgSetting01.getOldPaths().size(), is(1));
+        assertTrue(testPkgSetting01.getOldPaths().contains(INITIAL_CODE_PATH));
+        Settings.updatePackageSetting(
+                testPkgSetting01,
+                null /*disabledPkg*/,
+                null /*existingSharedUserSetting*/,
+                null /*sharedUser*/,
+                UPDATED_CODE_PATH2 /*codePath*/,
+                null /*legacyNativeLibraryPath*/,
+                "arm64-v8a" /*primaryCpuAbi*/,
+                "armeabi" /*secondaryCpuAbi*/,
+                ApplicationInfo.FLAG_SYSTEM /*pkgFlags*/,
+                ApplicationInfo.PRIVATE_FLAG_PRIVILEGED /*pkgPrivateFlags*/,
+                UserManagerService.getInstance(),
+                null /*usesSdkLibraries*/,
+                null /*usesSdkLibrariesVersions*/,
+                null /*usesSdkLibrariesOptional*/,
+                null /*usesStaticLibraries*/,
+                null /*usesStaticLibrariesVersions*/,
+                null /*mimeGroups*/,
+                UUID.randomUUID(),
+                34 /*targetSdkVersion*/,
+                null /*restrictUpdateHash*/,
+                true /*isDontKill*/);
+        assertThat(testPkgSetting01.getPath(), is(UPDATED_CODE_PATH2));
+        assertThat(testPkgSetting01.getOldPaths().size(), is(2));
+        int index = 0;
+        for (File path : testPkgSetting01.getOldPaths()) {
+            switch (index) {
+                case 0 -> assertThat(path, is(INITIAL_CODE_PATH));
+                case 1 -> assertThat(path, is(UPDATED_CODE_PATH));
+            }
+            index++;
+        }
+    }
+
     /** Update package; changing shared user throws exception */
     @Test
     public void testUpdatePackageSetting03() {
@@ -1266,7 +1354,8 @@
                     null /*mimeGroups*/,
                     UUID.randomUUID(),
                     34 /*targetSdkVersion*/,
-                    null /*restrictUpdateHash*/);
+                    null /*restrictUpdateHash*/,
+                    false /*isDontKill*/);
             fail("Expected a PackageManagerException");
         } catch (PackageManagerException expected) {
         }
@@ -1672,6 +1761,63 @@
         assertSame(sus1, settings.getSharedUserSettingLPr(ps1));
     }
 
+    @Test
+    public void testAddRemoveOldPaths() {
+        final PackageSetting ps1 = createPackageSetting(PACKAGE_NAME_1);
+        ps1.addOldPath(INITIAL_CODE_PATH);
+        ps1.addOldPath(UPDATED_CODE_PATH);
+        ps1.addOldPath(UPDATED_CODE_PATH2);
+        assertThat(ps1.getOldPaths().size(), is(3));
+        int index = 0;
+        for (File path : ps1.getOldPaths()) {
+            switch (index) {
+                case 0 -> assertThat(path, is(INITIAL_CODE_PATH));
+                case 1 -> assertThat(path, is(UPDATED_CODE_PATH));
+                case 2 -> assertThat(path, is(UPDATED_CODE_PATH2));
+            }
+            index++;
+        }
+
+        ps1.removeOldPath(UPDATED_CODE_PATH);
+        assertThat(ps1.getOldPaths().size(), is(2));
+        index = 0;
+        for (File path : ps1.getOldPaths()) {
+            switch (index) {
+                case 0 -> assertThat(path, is(INITIAL_CODE_PATH));
+                case 1 -> assertThat(path, is(UPDATED_CODE_PATH2));
+            }
+            index++;
+        }
+        ps1.removeOldPath(UPDATED_CODE_PATH2);
+        // assert not throws when deleting non-existing element
+        ps1.removeOldPath(UPDATED_CODE_PATH2);
+        assertThat(ps1.getOldPaths().size(), is(1));
+        assertTrue(ps1.getOldPaths().contains(INITIAL_CODE_PATH));
+        ps1.removeOldPath(INITIAL_CODE_PATH);
+        // assert not throws when deleting from an empty list
+        ps1.removeOldPath(INITIAL_CODE_PATH);
+        assertThat(ps1.getOldPaths().size(), is(0));
+    }
+
+    @Test
+    public void testOldPathsNotPreservedAfterReboot() {
+        Settings settings = makeSettings();
+        final PackageSetting ps1 = createPackageSetting(PACKAGE_NAME_1);
+        ps1.setAppId(Process.FIRST_APPLICATION_UID);
+        ps1.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed()
+                .setUid(ps1.getAppId())
+                .hideAsFinal());
+        ps1.addOldPath(INITIAL_CODE_PATH);
+        ps1.addOldPath(UPDATED_CODE_PATH);
+        settings.mPackages.put(PACKAGE_NAME_1, ps1);
+
+        settings.writeLPr(computer, /*sync=*/true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        assertNull(settings.getPackageLPr(PACKAGE_NAME_1).getOldPaths());
+    }
+
     private void verifyUserState(PackageUserState userState,
             boolean notLaunched, boolean stopped, boolean installed) {
         assertThat(userState.getEnabledState(), is(0));
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
index 03e45a2..71f5c75 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
@@ -58,6 +58,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.pm.parsing.PackageParser2;
+import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.PackageImpl;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.pm.permission.CompatibilityPermissionInfo;
@@ -84,7 +86,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.server.pm.parsing.PackageCacher;
 import com.android.server.pm.parsing.PackageInfoUtils;
-import com.android.server.pm.parsing.PackageParser2;
+import com.android.server.pm.parsing.PackageParserUtils;
 import com.android.server.pm.parsing.TestPackageParser2;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -185,7 +187,7 @@
 
     @Test
     public void test_serializePackage() throws Exception {
-        try (PackageParser2 pp = PackageParser2.forParsingFileWithDefaults()) {
+        try (PackageParser2 pp = PackageParserUtils.forParsingFileWithDefaults()) {
             AndroidPackage pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */,
                     true /* useCaches */).hideAsFinal();
 
@@ -363,7 +365,7 @@
                     actualDisplayCategory = activity.getRequiredDisplayCategory();
                 }
             }
-        } catch (PackageManagerException e) {
+        } catch (PackageParserException e) {
             assertThat(e.getMessage()).contains(
                     "requiredDisplayCategory attribute can only consist"
                             + " of alphanumeric characters, '_', and '.'");
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
index c0c7032..9780440 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
@@ -27,6 +27,7 @@
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.content.pm.SuspendDialogInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.os.PersistableBundle;
 import android.platform.test.annotations.Presubmit;
@@ -89,7 +90,7 @@
         assertThat(testUserState.equals(oldUserState), is(false));
 
         oldUserState = new PackageUserStateImpl();
-        oldUserState.putSuspendParams("suspendingPackage",
+        oldUserState.putSuspendParams(UserPackage.of(0, "suspendingPackage"),
                 new SuspendParams(null, new PersistableBundle(), null));
         assertThat(testUserState.equals(oldUserState), is(false));
 
@@ -220,6 +221,8 @@
         final PersistableBundle launcherExtras2 = createPersistableBundle(null, 0, "name",
                 "launcherExtras2", null, 0);
 
+        final int suspendingUser1 = 0;
+        final int suspendingUser2 = 10;
         final String suspendingPackage1 = "package1";
         final String suspendingPackage2 = "package2";
 
@@ -230,12 +233,12 @@
                 .setMessage("dialogMessage2")
                 .build();
 
-        final ArrayMap<String, SuspendParams> paramsMap1 = new ArrayMap<>();
-        paramsMap1.put(suspendingPackage1, createSuspendParams(dialogInfo1, appExtras1,
-                launcherExtras1));
-        final ArrayMap<String, SuspendParams> paramsMap2 = new ArrayMap<>();
-        paramsMap2.put(suspendingPackage2, createSuspendParams(dialogInfo2,
-                appExtras2, launcherExtras2));
+        final ArrayMap<UserPackage, SuspendParams> paramsMap1 = new ArrayMap<>();
+        paramsMap1.put(UserPackage.of(suspendingUser1, suspendingPackage1),
+                createSuspendParams(dialogInfo1, appExtras1, launcherExtras1));
+        final ArrayMap<UserPackage, SuspendParams> paramsMap2 = new ArrayMap<>();
+        paramsMap2.put(UserPackage.of(suspendingUser2, suspendingPackage2),
+                createSuspendParams(dialogInfo2, appExtras2, launcherExtras2));
 
 
         final PackageUserStateImpl testUserState1 = new PackageUserStateImpl();
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
index 8a74e24..3761240 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
@@ -21,8 +21,8 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.TestPackageParser2;
 
 import junit.framework.Assert;
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
index b63950c..a28b28a 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
@@ -38,6 +38,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.pm.pkg.component.ParsedActivityUtils;
 import com.android.internal.pm.pkg.component.ParsedComponent;
@@ -45,7 +46,6 @@
 import com.android.internal.pm.pkg.component.ParsedPermission;
 import com.android.internal.pm.pkg.component.ParsedPermissionUtils;
 import com.android.internal.util.ArrayUtils;
-import com.android.server.pm.PackageManagerException;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.test.service.server.R;
 
@@ -608,7 +608,7 @@
             try {
                 parsePackage(filename, resId, x -> x);
                 expect.withMessage("Expected parsing error %s from %s", result, filename).fail();
-            } catch (PackageManagerException expected) {
+            } catch (PackageParserException expected) {
                 expect.that(expected.error).isEqualTo(result);
             }
         }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
index 98af63c..1d668cd 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
@@ -18,8 +18,8 @@
 
 import android.content.pm.PackageManager
 import android.platform.test.annotations.Postsubmit
+import com.android.internal.pm.parsing.PackageParserException
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils
-import com.android.server.pm.PackageManagerException
 import com.android.server.pm.PackageManagerService
 import com.android.server.pm.PackageManagerServiceUtils
 import java.io.File
@@ -39,7 +39,7 @@
 @Postsubmit
 class SystemPartitionParseTest {
 
-    private val parser = PackageParser2.forParsingFileWithDefaults()
+    private val parser = PackageParserUtils.forParsingFileWithDefaults()
 
     @get:Rule
     val tempFolder = TemporaryFolder()
@@ -86,7 +86,7 @@
                     }
                 }
                 .mapNotNull { it.exceptionOrNull() }
-                .filterNot { (it as? PackageManagerException)?.error ==
+                .filterNot { (it as? PackageParserException)?.error ==
                         PackageManager.INSTALL_PARSE_FAILED_SKIPPED }
 
         if (exceptions.isEmpty()) return
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 2bdebe2..418b78c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -17,6 +17,8 @@
 package com.android.server.display;
 
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -39,7 +41,9 @@
 import android.hardware.SensorManager;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.os.test.TestLooper;
+import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -98,8 +102,8 @@
 
         mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
         mContext = InstrumentationRegistry.getContext();
-        setupController(BrightnessMappingStrategy.NO_USER_LUX,
-                BrightnessMappingStrategy.NO_USER_BRIGHTNESS, /* applyDebounce= */ false,
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ false,
                 /* useHorizon= */ true);
     }
 
@@ -112,11 +116,19 @@
         }
     }
 
-    private void setupController(float userLux, float userBrightness, boolean applyDebounce,
+    private void setupController(float userLux, float userNits, boolean applyDebounce,
             boolean useHorizon) {
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
 
+        when(mBrightnessMappingStrategy.getMode()).thenReturn(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        when(mIdleBrightnessMappingStrategy.getMode()).thenReturn(AUTO_BRIGHTNESS_MODE_IDLE);
+
+        SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap = new SparseArray<>();
+        brightnessMappingStrategyMap.append(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                mBrightnessMappingStrategy);
+        brightnessMappingStrategyMap.append(AUTO_BRIGHTNESS_MODE_IDLE,
+                mIdleBrightnessMappingStrategy);
         mController = new AutomaticBrightnessController(
                 new AutomaticBrightnessController.Injector() {
                     @Override
@@ -131,7 +143,7 @@
 
                 }, // pass in test looper instead, pass in offsettable clock
                 () -> { }, mTestLooper.getLooper(), mSensorManager, mLightSensor,
-                mBrightnessMappingStrategy, LIGHT_SENSOR_WARMUP_TIME, BRIGHTNESS_MIN_FLOAT,
+                brightnessMappingStrategyMap, LIGHT_SENSOR_WARMUP_TIME, BRIGHTNESS_MIN_FLOAT,
                 BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, LIGHT_SENSOR_RATE,
                 INITIAL_LIGHT_SENSOR_RATE, applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG : 0,
                 applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG : 0,
@@ -141,8 +153,8 @@
                 mAmbientBrightnessThresholds, mScreenBrightnessThresholds,
                 mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
-                mIdleBrightnessMappingStrategy, useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
-                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userBrightness
+                useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
+                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
@@ -326,8 +338,7 @@
 
         when(mBrightnessMappingStrategy.getShortTermModelTimeout()).thenReturn(2000L);
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
         when(mBrightnessMappingStrategy.shouldResetShortTermModel(
                 123f, 0.5f)).thenReturn(true);
 
@@ -337,7 +348,7 @@
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
 
-        mController.switchToInteractiveScreenBrightnessMode();
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
         mTestLooper.moveTimeForward(4000);
         mTestLooper.dispatchAll();
 
@@ -371,15 +382,14 @@
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(0.51f);
         when(mBrightnessMappingStrategy.getUserLux()).thenReturn(123.0f);
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         // Time does not move forward, since clock is doesn't increment naturally.
         mTestLooper.dispatchAll();
 
         // Sensor reads 100000 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 678910));
-        mController.switchToInteractiveScreenBrightnessMode();
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
 
         // Verify short term model is not reset.
         verify(mBrightnessMappingStrategy, never()).clearUserDataPoints();
@@ -410,10 +420,11 @@
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(0.5f);
         when(mBrightnessMappingStrategy.getUserLux()).thenReturn(123f);
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
-        when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(-1f);
-        when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(-1f);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(
+                PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(
+                BrightnessMappingStrategy.INVALID_LUX);
         when(mBrightnessMappingStrategy.shouldResetShortTermModel(
                 123f, 0.5f)).thenReturn(true);
 
@@ -423,7 +434,7 @@
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
 
-        mController.switchToInteractiveScreenBrightnessMode();
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
         mTestLooper.moveTimeForward(4000);
         mTestLooper.dispatchAll();
 
@@ -456,10 +467,11 @@
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(0.5f);
         when(mBrightnessMappingStrategy.getUserLux()).thenReturn(123f);
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
-        when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(-1f);
-        when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(-1f);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(
+                PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(
+                BrightnessMappingStrategy.INVALID_LUX);
 
         when(mBrightnessMappingStrategy.shouldResetShortTermModel(
                 123f, 0.5f)).thenReturn(true);
@@ -469,7 +481,7 @@
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
-        mController.switchToInteractiveScreenBrightnessMode();
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
         // Do not fast-forward time
         mTestLooper.dispatchAll();
 
@@ -492,22 +504,25 @@
         // Sensor reads 123 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         when(mBrightnessMappingStrategy.getShortTermModelTimeout()).thenReturn(2000L);
-        when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(-1.0f);
-        when(mBrightnessMappingStrategy.getUserLux()).thenReturn(-1.0f);
+        when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(
+                PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mBrightnessMappingStrategy.getUserLux()).thenReturn(
+                BrightnessMappingStrategy.INVALID_LUX);
 
         // No user brightness interaction.
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
-        when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(-1.0f);
-        when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(-1.0f);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(
+                PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(
+                BrightnessMappingStrategy.INVALID_LUX);
 
         // Sensor reads 1000 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
-        mController.switchToInteractiveScreenBrightnessMode();
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
         // Do not fast-forward time
         mTestLooper.dispatchAll();
 
@@ -541,12 +556,12 @@
         verify(mBrightnessMappingStrategy, times(3)).getBrightness(anyFloat(), any(), anyInt());
 
         // Now let's do the same for idle mode
-        mController.switchToIdleMode();
-        // Called once for init, and once when switching,
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        // Called once when switching,
         // setAmbientLux() is called twice and once in updateAutoBrightness(),
         // nextAmbientLightBrighteningTransition() and nextAmbientLightDarkeningTransition() are
         // called twice each.
-        verify(mBrightnessMappingStrategy, times(9)).isForIdleMode();
+        verify(mBrightnessMappingStrategy, times(8)).getMode();
         // Called when switching.
         verify(mBrightnessMappingStrategy, times(1)).getShortTermModelTimeout();
         verify(mBrightnessMappingStrategy, times(1)).getUserBrightness();
@@ -826,7 +841,6 @@
 
     @Test
     public void testResetShortTermModelWhenConfigChanges() {
-        when(mBrightnessMappingStrategy.isForIdleMode()).thenReturn(false);
         when(mBrightnessMappingStrategy.setBrightnessConfiguration(any())).thenReturn(true);
 
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -847,8 +861,10 @@
         verify(mBrightnessMappingStrategy, never()).addUserDataPoint(anyFloat(), anyFloat());
 
         float userLux = 1000;
+        float userNits = 500;
         float userBrightness = 0.3f;
-        setupController(userLux, userBrightness, /* applyDebounce= */ true,
+        when(mBrightnessMappingStrategy.getBrightnessFromNits(userNits)).thenReturn(userBrightness);
+        setupController(userLux, userNits, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
         verify(mBrightnessMappingStrategy).addUserDataPoint(userLux, userBrightness);
     }
@@ -856,8 +872,8 @@
     @Test
     public void testBrighteningLightDebounce() throws Exception {
         clearInvocations(mSensorManager);
-        setupController(BrightnessMappingStrategy.NO_USER_LUX,
-                BrightnessMappingStrategy.NO_USER_BRIGHTNESS, /* applyDebounce= */ true,
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
 
         ArgumentCaptor<SensorEventListener> listenerCaptor =
@@ -897,8 +913,8 @@
                 .thenReturn(10000f);
         when(mAmbientBrightnessThresholds.getDarkeningThreshold(anyFloat()))
                 .thenReturn(10000f);
-        setupController(BrightnessMappingStrategy.NO_USER_LUX,
-                BrightnessMappingStrategy.NO_USER_BRIGHTNESS, /* applyDebounce= */ true,
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
 
         ArgumentCaptor<SensorEventListener> listenerCaptor =
@@ -934,12 +950,11 @@
     @Test
     public void testBrighteningLightDebounceIdle() throws Exception {
         clearInvocations(mSensorManager);
-        setupController(BrightnessMappingStrategy.NO_USER_LUX,
-                BrightnessMappingStrategy.NO_USER_BRIGHTNESS, /* applyDebounce= */ true,
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
@@ -972,12 +987,11 @@
                 .thenReturn(10000f);
         when(mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(anyFloat()))
                 .thenReturn(10000f);
-        setupController(BrightnessMappingStrategy.NO_USER_LUX,
-                BrightnessMappingStrategy.NO_USER_BRIGHTNESS, /* applyDebounce= */ true,
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
 
-        mController.switchToIdleMode();
-        when(mIdleBrightnessMappingStrategy.isForIdleMode()).thenReturn(true);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
index a2e80f0..fb73aff 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
@@ -16,6 +16,10 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
@@ -26,21 +30,22 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.hardware.display.BrightnessConfiguration;
 import android.os.PowerManager;
+import android.provider.Settings;
+import android.testing.TestableContext;
 import android.util.MathUtils;
 import android.util.Spline;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
-
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 
 import java.util.Arrays;
 
@@ -104,20 +109,6 @@
         468.5f,
     };
 
-    private static final int[] DISPLAY_LEVELS_INT = {
-        9,
-        30,
-        45,
-        62,
-        78,
-        96,
-        119,
-        146,
-        178,
-        221,
-        255
-    };
-
     private static final float[] DISPLAY_LEVELS = {
         0.03f,
         0.11f,
@@ -165,58 +156,35 @@
 
     private static final float TOLERANCE = 0.0001f;
 
-    @Mock
-    DisplayWhiteBalanceController mMockDwbc;
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            InstrumentationRegistry.getInstrumentation().getContext());
+
+    @Before
+    public void setUp() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL);
+    }
 
     @Test
-    public void testSimpleStrategyMappingAtControlPoints_IntConfig() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+    public void testSimpleStrategyMappingAtControlPoints() {
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNotNull("BrightnessMappingStrategy should not be null", simple);
         for (int i = 0; i < LUX_LEVELS.length; i++) {
-            final float expectedLevel = MathUtils.map(PowerManager.BRIGHTNESS_OFF + 1,
-                    PowerManager.BRIGHTNESS_ON, PowerManager.BRIGHTNESS_MIN,
-                    PowerManager.BRIGHTNESS_MAX, DISPLAY_LEVELS_INT[i]);
-            assertEquals(expectedLevel,
-                    simple.getBrightness(LUX_LEVELS[i]), 0.0001f /*tolerance*/);
+            assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]), TOLERANCE);
         }
     }
 
     @Test
-    public void testSimpleStrategyMappingBetweenControlPoints_IntConfig() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", simple);
-        for (int i = 1; i < LUX_LEVELS.length; i++) {
-            final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2;
-            final float backlight = simple.getBrightness(lux) * PowerManager.BRIGHTNESS_ON;
-            assertTrue("Desired brightness should be between adjacent control points.",
-                    backlight > DISPLAY_LEVELS_INT[i - 1]
-                            && backlight < DISPLAY_LEVELS_INT[i]);
-        }
-    }
-
-    @Test
-    public void testSimpleStrategyMappingAtControlPoints_FloatConfig() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY, EMPTY_FLOAT_ARRAY, LUX_LEVELS,
-                EMPTY_FLOAT_ARRAY, DISPLAY_LEVELS);
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", simple);
-        for (int i = 0; i < LUX_LEVELS.length; i++) {
-            assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]),
-                    /* tolerance= */ 0.0001f);
-        }
-    }
-
-    @Test
-    public void testSimpleStrategyMappingBetweenControlPoints_FloatConfig() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY, EMPTY_FLOAT_ARRAY, LUX_LEVELS,
-                EMPTY_FLOAT_ARRAY, DISPLAY_LEVELS);
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+    public void testSimpleStrategyMappingBetweenControlPoints() {
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNotNull("BrightnessMappingStrategy should not be null", simple);
         for (int i = 1; i < LUX_LEVELS.length; i++) {
             final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2;
@@ -228,9 +196,10 @@
 
     @Test
     public void testSimpleStrategyIgnoresNewConfiguration() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
 
         final float[] lux = { 0f, 1f };
         final float[] nits = { 0, PowerManager.BRIGHTNESS_ON };
@@ -243,25 +212,25 @@
 
     @Test
     public void testSimpleStrategyIgnoresNullConfiguration() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
 
         strategy.setBrightnessConfiguration(null);
-        final int n = DISPLAY_LEVELS_INT.length;
-        final float expectedBrightness =
-                (float) DISPLAY_LEVELS_INT[n - 1] / PowerManager.BRIGHTNESS_ON;
+        final int n = DISPLAY_LEVELS.length;
+        final float expectedBrightness = DISPLAY_LEVELS[n - 1];
         assertEquals(expectedBrightness,
                 strategy.getBrightness(LUX_LEVELS[n - 1]), 0.0001f /*tolerance*/);
     }
 
     @Test
     public void testPhysicalStrategyMappingAtControlPoints() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
+        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNotNull("BrightnessMappingStrategy should not be null", physical);
         for (int i = 0; i < LUX_LEVELS.length; i++) {
             final float expectedLevel = MathUtils.map(DISPLAY_RANGE_NITS[0], DISPLAY_RANGE_NITS[1],
@@ -276,10 +245,11 @@
 
     @Test
     public void testPhysicalStrategyMappingBetweenControlPoints() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
+        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNotNull("BrightnessMappingStrategy should not be null", physical);
         Spline brightnessToNits =
                 Spline.createSpline(BACKLIGHT_RANGE_ZERO_TO_ONE, DISPLAY_RANGE_NITS);
@@ -294,10 +264,11 @@
 
     @Test
     public void testPhysicalStrategyUsesNewConfigurations() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
 
         final float[] lux = {0f, 1f};
         final float[] nits = {
@@ -320,10 +291,11 @@
 
     @Test
     public void testPhysicalStrategyRecalculateSplines() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         float[] adjustedNits50p = new float[DISPLAY_RANGE_NITS.length];
         for (int i = 0; i < DISPLAY_RANGE_NITS.length; i++) {
             adjustedNits50p[i] = DISPLAY_RANGE_NITS[i] * 0.5f;
@@ -364,10 +336,12 @@
 
     @Test
     public void testDefaultStrategyIsPhysical() {
-        Resources res = createResources(DISPLAY_LEVELS_INT);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevels(DISPLAY_LEVELS)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertTrue(strategy instanceof BrightnessMappingStrategy.PhysicalMappingStrategy);
     }
 
@@ -378,17 +352,19 @@
         float tmp = lux[idx];
         lux[idx] = lux[idx + 1];
         lux[idx + 1] = tmp;
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNull(strategy);
 
         // And make sure we get the same result even if it's monotone but not increasing.
         lux[idx] = lux[idx + 1];
-        ddc = createDdc(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux,
-                DISPLAY_LEVELS_NITS);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
+        strategy = BrightnessMappingStrategy.create(mContext, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
+                /* displayWhiteBalanceController= */ null);
         assertNull(strategy);
     }
 
@@ -399,72 +375,74 @@
         // Make sure it's strictly increasing so that the only failure is the differing array
         // lengths
         lux[lux.length - 1] = lux[lux.length - 2] + 1;
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNull(strategy);
 
-        res = createResources(DISPLAY_LEVELS_INT);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        ddc = new DdcBuilder().setAutoBrightnessLevelsLux(lux)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS)
+                .setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        strategy = BrightnessMappingStrategy.create(mContext, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
+                /* displayWhiteBalanceController= */ null);
         assertNull(strategy);
 
         // Extra backlight level
-        final int[] backlight = Arrays.copyOf(
-                DISPLAY_LEVELS_INT, DISPLAY_LEVELS_INT.length + 1);
+        final float[] backlight = Arrays.copyOf(DISPLAY_LEVELS, DISPLAY_LEVELS.length + 1);
         backlight[backlight.length - 1] = backlight[backlight.length - 2] + 1;
-        res = createResources(backlight);
-        ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, EMPTY_FLOAT_ARRAY);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        ddc = new DdcBuilder().setAutoBrightnessLevels(backlight).build();
+        strategy = BrightnessMappingStrategy.create(mContext, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
+                /* displayWhiteBalanceController= */ null);
         assertNull(strategy);
 
         // Extra nits level
         final float[] nits = Arrays.copyOf(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_NITS.length + 1);
         nits[nits.length - 1] = nits[nits.length - 2] + 1;
-        res = createResources(EMPTY_INT_ARRAY);
-        ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, nits);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        ddc = new DdcBuilder().setAutoBrightnessLevelsNits(nits)
+                .setAutoBrightnessLevels(EMPTY_FLOAT_ARRAY).build();
+        strategy = BrightnessMappingStrategy.create(mContext, ddc, AUTO_BRIGHTNESS_MODE_DEFAULT,
+                /* displayWhiteBalanceController= */ null);
         assertNull(strategy);
     }
 
     @Test
     public void testPhysicalStrategyRequiresNitsMapping() {
-        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY /*nitsRange*/);
-        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(physical);
-
-        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(physical);
-
-        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setNitsRange(EMPTY_FLOAT_ARRAY).build();
+        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertNull(physical);
     }
 
     @Test
     public void testStrategiesAdaptToUserDataPoint() {
-        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, mMockDwbc));
-        ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
-        res = createResources(DISPLAY_LEVELS_INT);
-        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, mMockDwbc));
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .setAutoBrightnessLevelsNits(DISPLAY_LEVELS_NITS).build();
+        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null));
+        ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .setAutoBrightnessLevels(DISPLAY_LEVELS).build();
+        setUpResources();
+        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null));
     }
 
     @Test
     public void testIdleModeConfigLoadsCorrectly() {
-        Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
+        setUpResources(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .build();
 
         // Create an idle mode bms
         // This will fail if it tries to fetch the wrong configuration.
-        BrightnessMappingStrategy bms = BrightnessMappingStrategy.createForIdleMode(res, ddc,
-                mMockDwbc);
+        BrightnessMappingStrategy bms = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_IDLE,
+                /* displayWhiteBalanceController= */ null);
         assertNotNull("BrightnessMappingStrategy should not be null", bms);
 
         // Ensure that the config is the one we set
@@ -532,81 +510,31 @@
         assertEquals(minBrightness, strategy.getBrightness(LUX_LEVELS[0]), 0.0001f /*tolerance*/);
     }
 
-    private Resources createResources(int[] brightnessLevelsBacklight) {
-        return createResources(brightnessLevelsBacklight, EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY);
+    private void setUpResources() {
+        setUpResources(EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY);
     }
 
-    private Resources createResourcesIdle(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) {
-        return createResources(EMPTY_INT_ARRAY,
-                luxLevelsIdle, brightnessLevelsNitsIdle);
-    }
-
-    private Resources createResources(int[] brightnessLevelsBacklight, int[] luxLevelsIdle,
-            float[] brightnessLevelsNitsIdle) {
-
-        Resources mockResources = mock(Resources.class);
+    private void setUpResources(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) {
         if (luxLevelsIdle.length > 0) {
             int[] luxLevelsIdleResource = Arrays.copyOfRange(luxLevelsIdle, 1,
                     luxLevelsIdle.length);
-            when(mockResources.getIntArray(
-                    com.android.internal.R.array.config_autoBrightnessLevelsIdle))
-                    .thenReturn(luxLevelsIdleResource);
+            mContext.getOrCreateTestableResources().addOverride(
+                    com.android.internal.R.array.config_autoBrightnessLevelsIdle,
+                    luxLevelsIdleResource);
         }
 
-        when(mockResources.getIntArray(
-                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
-                .thenReturn(brightnessLevelsBacklight);
-
         TypedArray mockBrightnessLevelNitsIdle = createFloatTypedArray(brightnessLevelsNitsIdle);
-        when(mockResources.obtainTypedArray(
-                com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle))
-                .thenReturn(mockBrightnessLevelNitsIdle);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle,
+                mockBrightnessLevelNitsIdle);
 
-        when(mockResources.getInteger(
-                com.android.internal.R.integer.config_screenBrightnessSettingMinimum))
-                .thenReturn(1);
-        when(mockResources.getInteger(
-                com.android.internal.R.integer.config_screenBrightnessSettingMaximum))
-                .thenReturn(255);
-        when(mockResources.getFraction(
-                com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma, 1, 1))
-                .thenReturn(MAXIMUM_GAMMA);
-        return mockResources;
-    }
-
-    private DisplayDeviceConfig createDdc() {
-        return createDdc(DISPLAY_RANGE_NITS);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray) {
-        return createDdc(nitsArray, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray) {
-        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
-        when(mockDdc.getNits()).thenReturn(nitsArray);
-        when(mockDdc.getBrightness()).thenReturn(backlightArray);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY);
-        when(mockDdc.getAutoBrightnessBrighteningLevels()).thenReturn(EMPTY_FLOAT_ARRAY);
-        return mockDdc;
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray,
-            float[] luxLevelsFloat, float[] brightnessLevelsNits) {
-        return createDdc(nitsArray, backlightArray, luxLevelsFloat, brightnessLevelsNits,
-                EMPTY_FLOAT_ARRAY);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray,
-            float[] luxLevelsFloat, float[] brightnessLevelsNits, float[] brightnessLevels) {
-        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
-        when(mockDdc.getNits()).thenReturn(nitsArray);
-        when(mockDdc.getBrightness()).thenReturn(backlightArray);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevelsFloat);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits);
-        when(mockDdc.getAutoBrightnessBrighteningLevels()).thenReturn(brightnessLevels);
-        return mockDdc;
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.integer.config_screenBrightnessSettingMinimum, 1);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.integer.config_screenBrightnessSettingMaximum, 255);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
+                MAXIMUM_GAMMA);
     }
 
     private TypedArray createFloatTypedArray(float[] vals) {
@@ -647,12 +575,11 @@
         final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
         final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
 
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         // Let's start with a validity check:
         assertEquals(y1, strategy.getBrightness(x1), 0.0001f /* tolerance */);
         assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
@@ -678,12 +605,11 @@
         final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1);
         final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
         final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         // Validity check:
         assertEquals(y1, strategy.getBrightness(x1), 0.0001f /* tolerance */);
         assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
@@ -706,12 +632,11 @@
     public void testGammaCorrectionExtremeChangeAtCenter() {
         // Extreme changes (e.g. setting brightness to 0.0 or 1.0) can't be gamma corrected, so we
         // just make sure the adjustment reflects the change.
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         assertEquals(0.0f, strategy.getAutoBrightnessAdjustment(), /* delta= */ 0.0001f);
         strategy.addUserDataPoint(/* lux= */ 2500, /* brightness= */ 1.0f);
         assertEquals(+1.0f, strategy.getAutoBrightnessAdjustment(), /* delta= */ 0.0001f);
@@ -730,12 +655,11 @@
         final float y0 = GAMMA_CORRECTION_SPLINE.interpolate(x0);
         final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
         final float y4 = GAMMA_CORRECTION_SPLINE.interpolate(x4);
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder().setAutoBrightnessLevelsLux(GAMMA_CORRECTION_LUX)
+                .setAutoBrightnessLevelsNits(GAMMA_CORRECTION_NITS).build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
         // Validity, as per tradition:
         assertEquals(y0, strategy.getBrightness(x0), 0.0001f /* tolerance */);
         assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
@@ -757,4 +681,96 @@
         assertEquals(1.0f, strategy.getBrightness(x4), 0.0001f /* tolerance */);
         assertEquals(adjustment, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
     }
+
+    @Test
+    public void testGetMode() {
+        setUpResources(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
+        DisplayDeviceConfig ddc = new DdcBuilder().setBrightnessRange(BACKLIGHT_RANGE_ZERO_TO_ONE)
+                .build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_IDLE, /* displayWhiteBalanceController= */ null);
+        assertEquals(AUTO_BRIGHTNESS_MODE_IDLE, strategy.getMode());
+    }
+
+    @Test
+    public void testAutoBrightnessModeAndPreset() {
+        int mode = AUTO_BRIGHTNESS_MODE_DOZE;
+        int preset = Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM;
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS, preset);
+
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder()
+                .setAutoBrightnessLevels(mode, preset, DISPLAY_LEVELS)
+                .setAutoBrightnessLevelsLux(mode, preset, LUX_LEVELS).build();
+        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(mContext, ddc, mode,
+                /* displayWhiteBalanceController= */ null);
+        assertNotNull("BrightnessMappingStrategy should not be null", simple);
+        for (int i = 0; i < LUX_LEVELS.length; i++) {
+            assertEquals(DISPLAY_LEVELS[i], simple.getBrightness(LUX_LEVELS[i]), TOLERANCE);
+        }
+    }
+
+    private static class DdcBuilder {
+        private DisplayDeviceConfig mDdc;
+
+        DdcBuilder() {
+            mDdc = mock(DisplayDeviceConfig.class);
+            when(mDdc.getNits()).thenReturn(DISPLAY_RANGE_NITS);
+            when(mDdc.getBrightness()).thenReturn(DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT);
+            when(mDdc.getAutoBrightnessBrighteningLevelsLux(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                    Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL)).thenReturn(LUX_LEVELS);
+            when(mDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY);
+            when(mDdc.getAutoBrightnessBrighteningLevels(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                    Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL))
+                    .thenReturn(EMPTY_FLOAT_ARRAY);
+        }
+
+        DdcBuilder setNitsRange(float[] nitsArray) {
+            when(mDdc.getNits()).thenReturn(nitsArray);
+            return this;
+        }
+
+        DdcBuilder setBrightnessRange(float[] brightnessArray) {
+            when(mDdc.getBrightness()).thenReturn(brightnessArray);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevelsLux(float[] luxLevels) {
+            when(mDdc.getAutoBrightnessBrighteningLevelsLux(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                    Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL)).thenReturn(luxLevels);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevelsLux(
+                @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset,
+                float[] luxLevels) {
+            when(mDdc.getAutoBrightnessBrighteningLevelsLux(mode, preset)).thenReturn(luxLevels);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevelsNits(float[] brightnessLevelsNits) {
+            when(mDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevels(float[] brightnessLevels) {
+            when(mDdc.getAutoBrightnessBrighteningLevels(AUTO_BRIGHTNESS_MODE_DEFAULT,
+                    Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL))
+                    .thenReturn(brightnessLevels);
+            return this;
+        }
+
+        DdcBuilder setAutoBrightnessLevels(
+                @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset,
+                float[] brightnessLevels) {
+            when(mDdc.getAutoBrightnessBrighteningLevels(mode, preset))
+                    .thenReturn(brightnessLevels);
+            return this;
+        }
+
+        DisplayDeviceConfig build() {
+            return mDdc;
+        }
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 31d7e88..7a84406 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -17,6 +17,9 @@
 package com.android.server.display;
 
 
+import static com.android.internal.display.BrightnessSynchronizer.brightnessIntToFloat;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.config.SensorData.SupportedMode;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat;
@@ -39,6 +42,7 @@
 import android.content.res.TypedArray;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Temperature;
+import android.provider.Settings;
 import android.util.SparseArray;
 import android.util.Spline;
 import android.view.SurfaceControl;
@@ -47,7 +51,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
-import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.config.HdrBrightnessData;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -605,10 +608,15 @@
 
     private void verifyConfigValuesFromConfigResource() {
         assertNull(mDisplayDeviceConfig.getName());
-        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
-        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(),
+                new float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                AUTO_BRIGHTNESS_MODE_DEFAULT, Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL),
+                new float[]{0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                AUTO_BRIGHTNESS_MODE_DEFAULT, Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL),
+                new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
+                        brightnessIntToFloat(150)}, SMALL_DELTA);
 
         // Test thresholds
         assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
@@ -674,7 +682,7 @@
         assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type);
         assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name);
 
-        assertEquals(BrightnessSynchronizer.brightnessIntToFloat(35),
+        assertEquals(brightnessIntToFloat(35),
                 mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
     }
 
@@ -734,9 +742,40 @@
                 getValidProxSensor(), /* includeIdleMode= */ false));
 
         assertArrayEquals(new float[]{0.0f, 80},
-                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), ZERO_DELTA);
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), ZERO_DELTA);
         assertArrayEquals(new float[]{0.2f, 0.3f},
-                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(), SMALL_DELTA);
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 90},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.3f, 0.4f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 95},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_DOZE,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.35f, 0.45f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_DOZE,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 100},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_DOZE,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.4f, 0.5f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_DOZE,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), SMALL_DELTA);
     }
 
     @Test
@@ -746,9 +785,15 @@
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
                 getValidProxSensor(), /* includeIdleMode= */ false));
 
-        assertNull(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels());
+        assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
+                        brightnessIntToFloat(150)},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), SMALL_DELTA);
         assertArrayEquals(new float[]{0, 110, 500},
-                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), ZERO_DELTA);
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), ZERO_DELTA);
         assertArrayEquals(new float[]{2, 200, 600},
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), SMALL_DELTA);
     }
@@ -1138,6 +1183,46 @@
                 +               "</point>\n"
                 +           "</map>\n"
                 +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<setting>dim</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.3</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>90</first>\n"
+                +                   "<second>0.4</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<mode>doze</mode>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.35</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>95</first>\n"
+                +                   "<second>0.45</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<mode>doze</mode>\n"
+                +           "<setting>bright</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.4</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>100</first>\n"
+                +                   "<second>0.5</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
                 +   "</autoBrightness>\n"
                 +  getPowerThrottlingConfig()
                 +   "<highBrightnessMode enabled=\"true\">\n"
@@ -1435,6 +1520,10 @@
         when(mResources.getIntArray(
                 com.android.internal.R.array.config_autoBrightnessLevels))
                 .thenReturn(screenBrightnessLevelLux);
+        int[] screenBrightnessLevels = new int[]{50, 100, 150};
+        when(mResources.getIntArray(
+                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
+                .thenReturn(screenBrightnessLevels);
 
         // Thresholds
         // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index dea838d..fbb14c3 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -88,4 +89,12 @@
 
         verify(mDisplayPowerController).setBrightnessFromOffload(brightness);
     }
+
+    @Test
+    public void testBlockScreenOn() {
+        Runnable unblocker = () -> {};
+        mSession.blockScreenOn(unblocker);
+
+        verify(mDisplayOffloader).onBlockingScreenOn(eq(unblocker));
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index acd9dce..4cc68cf 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -18,6 +18,9 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -28,7 +31,6 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
@@ -62,6 +64,7 @@
 import android.provider.Settings;
 import android.testing.TestableContext;
 import android.util.FloatProperty;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayInfo;
 
@@ -357,7 +360,7 @@
         float followerBrightness = 0.4f;
         float nits = 300;
         when(mHolder.automaticBrightnessController.convertToNits(leadBrightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(followerBrightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
         listener.onBrightnessChanged(leadBrightness);
@@ -372,7 +375,7 @@
         float brightness = 0.6f;
         nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -404,7 +407,7 @@
 
         float brightness = 0.3f;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(300f);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
                 .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -466,7 +469,7 @@
 
         float brightness = 0.3f;
         when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
                 .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -500,7 +503,7 @@
         when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
                 .thenReturn(nits);
         when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(followerBrightness);
 
         mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
@@ -534,7 +537,7 @@
         when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
                 .thenReturn(nits);
         when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(followerBrightness);
 
         mHolder.dpc.updateBrightness();
@@ -592,9 +595,9 @@
         float brightness = 0.6f;
         float nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -625,9 +628,9 @@
         brightness = 0.7f;
         nits = 700;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -697,9 +700,9 @@
         float brightness = 0.6f;
         float nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
-        when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -1124,7 +1127,7 @@
 
         float newBrightness = 0.4f;
         when(mHolder.brightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
-        when(mHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(mHolder.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(newBrightness);
         // New display device
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
@@ -1141,12 +1144,9 @@
     @Test
     public void testShortTermModelPersistsWhenDisplayDeviceChanges() {
         float lux = 2000;
-        float brightness = 0.4f;
         float nits = 500;
-        when(mHolder.brightnessMappingStrategy.getUserLux()).thenReturn(lux);
-        when(mHolder.brightnessMappingStrategy.getUserBrightness()).thenReturn(brightness);
-        when(mHolder.brightnessMappingStrategy.convertToNits(brightness)).thenReturn(nits);
-        when(mHolder.brightnessMappingStrategy.convertToFloatScale(nits)).thenReturn(brightness);
+        when(mHolder.automaticBrightnessController.getUserLux()).thenReturn(lux);
+        when(mHolder.automaticBrightnessController.getUserNits()).thenReturn(nits);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1);
@@ -1163,7 +1163,7 @@
                 any(Looper.class),
                 eq(mSensorManagerMock),
                 /* lightSensor= */ any(),
-                eq(mHolder.brightnessMappingStrategy),
+                /* brightnessMappingStrategyMap= */ any(SparseArray.class),
                 /* lightSensorWarmUpTime= */ anyInt(),
                 /* brightnessMin= */ anyFloat(),
                 /* brightnessMax= */ anyFloat(),
@@ -1182,11 +1182,10 @@
                 eq(mContext),
                 any(BrightnessRangeController.class),
                 any(BrightnessThrottler.class),
-                /* idleModeBrightnessMapper= */ isNull(),
                 /* ambientLightHorizonShort= */ anyInt(),
                 /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
-                eq(brightness)
+                eq(nits)
         );
     }
 
@@ -1219,7 +1218,7 @@
     public void testDwbcCallsHappenOnHandler() {
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
 
-        mHolder.dpc.setAutomaticScreenBrightnessMode(true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         verify(mDisplayWhiteBalanceControllerMock, never()).setStrongModeEnabled(true);
 
         // dispatch handler looper
@@ -1382,7 +1381,7 @@
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
         // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(/* idle= */ true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
         // A second time, when switching to idle mode.
@@ -1411,7 +1410,7 @@
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
         // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(/* idle= */ true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
         // A second time, when switching to idle mode.
@@ -1437,7 +1436,7 @@
                 mHolder.config, /* isEnabled= */ true);
 
         // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         // A second time when switching to idle mode.
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
@@ -1463,7 +1462,7 @@
                 mHolder.config, /* isEnabled= */ true);
 
         // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
@@ -1571,6 +1570,56 @@
                 eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
     }
 
+    @Test
+    public void testSwitchToDozeAutoBrightnessMode() {
+        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by handleBrightnessModeChange, another triggered by requestPowerState
+        verify(mHolder.automaticBrightnessController, times(2))
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+
+        // Back to default mode
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+    }
+
+    @Test
+    public void testDoesNotSwitchFromIdleToDozeAutoBrightnessMode() {
+        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        when(mHolder.automaticBrightnessController.isInIdleMode()).thenReturn(true);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController, never())
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+    }
+
+    @Test
+    public void testDoesNotSwitchDozeAutoBrightnessModeIfFeatureFlagOff() {
+        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(false);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController, never())
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+    }
+
     /**
      * Creates a mock and registers it to {@link LocalServices}.
      */
@@ -1839,7 +1888,7 @@
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
-                BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
                 int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
                 float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
                 long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
@@ -1850,15 +1899,13 @@
                 HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler,
-                BrightnessMappingStrategy idleModeBrightnessMapper,
-                int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
-                float userBrightness) {
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits) {
             return mAutomaticBrightnessController;
         }
 
         @Override
-        BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
                 DisplayDeviceConfig displayDeviceConfig,
                 DisplayWhiteBalanceController displayWhiteBalanceController) {
             return mBrightnessMappingStrategy;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 50b0e16..943862f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -28,7 +29,6 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
@@ -62,6 +62,7 @@
 import android.provider.Settings;
 import android.testing.TestableContext;
 import android.util.FloatProperty;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayInfo;
 
@@ -357,7 +358,7 @@
         float followerBrightness = 0.4f;
         float nits = 300;
         when(mHolder.automaticBrightnessController.convertToNits(leadBrightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(followerBrightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
         listener.onBrightnessChanged(leadBrightness);
@@ -375,7 +376,7 @@
         float brightness = 0.6f;
         nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -408,7 +409,7 @@
 
         float brightness = 0.3f;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(300f);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
                 .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -472,7 +473,7 @@
 
         float brightness = 0.3f;
         when(mHolder.automaticBrightnessController.convertToNits(anyFloat())).thenReturn(-1f);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(anyFloat()))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(anyFloat()))
                 .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -507,7 +508,7 @@
         when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
                 .thenReturn(nits);
         when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(followerBrightness);
 
         mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
@@ -541,7 +542,7 @@
         when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
                 .thenReturn(nits);
         when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(followerBrightness);
 
         mHolder.dpc.updateBrightness();
@@ -600,9 +601,9 @@
         float brightness = 0.6f;
         float nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -633,9 +634,9 @@
         brightness = 0.7f;
         nits = 700;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
-        when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerDpc.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -706,9 +707,9 @@
         float brightness = 0.6f;
         float nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
-        when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerHolder.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
@@ -1056,7 +1057,7 @@
 
         float newBrightness = 0.4f;
         when(mHolder.brightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
-        when(mHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(mHolder.automaticBrightnessController.getBrightnessFromNits(nits))
                 .thenReturn(newBrightness);
         // New display device
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
@@ -1073,12 +1074,9 @@
     @Test
     public void testShortTermModelPersistsWhenDisplayDeviceChanges() {
         float lux = 2000;
-        float brightness = 0.4f;
         float nits = 500;
-        when(mHolder.brightnessMappingStrategy.getUserLux()).thenReturn(lux);
-        when(mHolder.brightnessMappingStrategy.getUserBrightness()).thenReturn(brightness);
-        when(mHolder.brightnessMappingStrategy.convertToNits(brightness)).thenReturn(nits);
-        when(mHolder.brightnessMappingStrategy.convertToFloatScale(nits)).thenReturn(brightness);
+        when(mHolder.automaticBrightnessController.getUserLux()).thenReturn(lux);
+        when(mHolder.automaticBrightnessController.getUserNits()).thenReturn(nits);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1);
@@ -1095,7 +1093,7 @@
                 any(Looper.class),
                 eq(mSensorManagerMock),
                 /* lightSensor= */ any(),
-                eq(mHolder.brightnessMappingStrategy),
+                /* brightnessMappingStrategyMap= */ any(SparseArray.class),
                 /* lightSensorWarmUpTime= */ anyInt(),
                 /* brightnessMin= */ anyFloat(),
                 /* brightnessMax= */ anyFloat(),
@@ -1114,11 +1112,10 @@
                 eq(mContext),
                 any(BrightnessRangeController.class),
                 any(BrightnessThrottler.class),
-                /* idleModeBrightnessMapper= */ isNull(),
                 /* ambientLightHorizonShort= */ anyInt(),
                 /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
-                eq(brightness)
+                eq(nits)
         );
     }
 
@@ -1151,7 +1148,7 @@
     public void testDwbcCallsHappenOnHandler() {
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
 
-        mHolder.dpc.setAutomaticScreenBrightnessMode(true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         verify(mDisplayWhiteBalanceControllerMock, never()).setStrongModeEnabled(true);
 
         // dispatch handler looper
@@ -1293,7 +1290,7 @@
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
         // switch to idle
-        mHolder.dpc.setAutomaticScreenBrightnessMode(/* idle= */ true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
@@ -1320,7 +1317,7 @@
                 BRIGHTNESS_RAMP_DECREASE_MAX);
 
         // switch to idle
-        mHolder.dpc.setAutomaticScreenBrightnessMode(/* idle= */ true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
         advanceTime(1);
 
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
@@ -1347,7 +1344,7 @@
                 mHolder.config, /* isEnabled= */ true);
 
         // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         // second time when switching to idle screen brightness mode
         verify(mHolder.animator, times(2)).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX,
@@ -1372,7 +1369,7 @@
                 mHolder.config, /* isEnabled= */ true);
 
         // switch to idle mode
-        mHolder.dpc.setAutomaticScreenBrightnessMode(true);
+        mHolder.dpc.setAutomaticScreenBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE);
 
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
@@ -1655,7 +1652,7 @@
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
                 SensorManager sensorManager, Sensor lightSensor,
-                BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
                 int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
                 float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
                 long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
@@ -1666,15 +1663,13 @@
                 HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler,
-                BrightnessMappingStrategy idleModeBrightnessMapper,
-                int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
-                float userBrightness) {
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits) {
             return mAutomaticBrightnessController;
         }
 
         @Override
-        BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+        BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
                 DisplayDeviceConfig displayDeviceConfig,
                 DisplayWhiteBalanceController displayWhiteBalanceController) {
             return mBrightnessMappingStrategy;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
index 49fa254..dafbbb3 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
@@ -23,8 +23,10 @@
 import org.junit.Test
 import org.mockito.junit.MockitoJUnit
 
+import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
+import java.util.concurrent.Executor
 
 @SmallTest
 class DisplayPowerStateTest {
@@ -36,15 +38,21 @@
 
     private val mockBlanker = mock<DisplayBlanker>()
     private val mockColorFade = mock<ColorFade>()
+    private val mockExecutor = mock<Executor>()
 
     @Before
     fun setUp() {
-        displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON)
+        displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON,
+                mockExecutor)
     }
 
     @Test
     fun `destroys ColorFade on stop`() {
         displayPowerState.stop()
+        val runnableCaptor = argumentCaptor<Runnable>()
+
+        verify(mockExecutor).execute(runnableCaptor.capture())
+        runnableCaptor.firstValue.run()
 
         verify(mockColorFade).destroy()
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index f36854b..00f9892 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -206,6 +206,9 @@
         when(mMockedResources.getIntArray(
             com.android.internal.R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
             .thenReturn(new int[]{});
+        when(mMockedResources.getIntArray(
+                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
+                .thenReturn(new int[]{});
         doReturn(true).when(mFlags).isDisplayOffloadEnabled();
         initDisplayOffloadSession();
     }
@@ -1235,6 +1238,9 @@
 
             @Override
             public void stopOffload() {}
+
+            @Override
+            public void onBlockingScreenOn(Runnable unblocker) {}
         });
 
         mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index 52fa91f..2d0c3fd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -293,21 +293,21 @@
     }
 
     @Test
-    public void testConvertToFloatScale() {
+    public void testGetBrightnessFromNits() {
         float brightness = 0.5f;
         float nits = 300;
 
         // ABC is null
         assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                mDisplayBrightnessController.convertToFloatScale(nits), /* delta= */ 0);
+                mDisplayBrightnessController.getBrightnessFromNits(nits), /* delta= */ 0);
 
         AutomaticBrightnessController automaticBrightnessController =
                 mock(AutomaticBrightnessController.class);
-        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
+        when(automaticBrightnessController.getBrightnessFromNits(nits)).thenReturn(brightness);
         mDisplayBrightnessController.setAutomaticBrightnessController(
                 automaticBrightnessController);
 
-        assertEquals(brightness, mDisplayBrightnessController.convertToFloatScale(nits),
+        assertEquals(brightness, mDisplayBrightnessController.getBrightnessFromNits(nits),
                 /* delta= */ 0);
     }
 
@@ -329,7 +329,7 @@
         float brightness = 0.3f;
         AutomaticBrightnessController automaticBrightnessController =
                 mock(AutomaticBrightnessController.class);
-        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
+        when(automaticBrightnessController.getBrightnessFromNits(nits)).thenReturn(brightness);
         when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
         mDisplayBrightnessController.setAutomaticBrightnessController(
                 automaticBrightnessController);
@@ -340,7 +340,7 @@
         // When the nits value is invalid, the brightness is resumed from where it was last set
         nits = -1;
         brightness = 0.4f;
-        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
+        when(automaticBrightnessController.getBrightnessFromNits(nits)).thenReturn(brightness);
         when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
         when(mBrightnessSetting.getBrightness()).thenReturn(brightness);
         mDisplayBrightnessController.setAutomaticBrightnessController(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
index 8d8274c..87fc7a4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -122,6 +122,16 @@
     }
 
     @Test
+    public void testRegisterHdrListener_ZeroMinHdrPercent() {
+        IBinder otherBinder = mock(IBinder.class);
+        mHdrClamper.resetHdrConfig(TEST_HDR_DATA, WIDTH, HEIGHT,
+            /* minimumHdrPercentOfScreen= */ 0, otherBinder);
+
+        verify(mMockHdrInfoListener).unregister(mMockBinder);
+        verify(mMockHdrInfoListener).register(otherBinder);
+    }
+
+    @Test
     public void testRegisterNotCalledIfHbmConfigIsMissing() {
         IBinder otherBinder = mock(IBinder.class);
         mHdrClamper.resetHdrConfig(TEST_HDR_DATA, WIDTH, HEIGHT, -1, otherBinder);
diff --git a/services/tests/media/OWNERS b/services/tests/media/OWNERS
new file mode 100644
index 0000000..160767a6
--- /dev/null
+++ b/services/tests/media/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 137631
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
diff --git a/services/tests/media/mediarouterservicetest/Android.bp b/services/tests/media/mediarouterservicetest/Android.bp
new file mode 100644
index 0000000..aed3af6
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/Android.bp
@@ -0,0 +1,39 @@
+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: "MediaRouterServiceTests",
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "androidx.test.runner",
+        "compatibility-device-util-axt",
+        "junit",
+        "platform-test-annotations",
+        "services.core",
+        "truth",
+    ],
+
+    platform_apis: true,
+
+    test_suites: [
+        // "device-tests",
+        "general-tests",
+    ],
+
+    certificate: "platform",
+    dxflags: ["--multi-dex"],
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/media/mediarouterservicetest/AndroidManifest.xml b/services/tests/media/mediarouterservicetest/AndroidManifest.xml
new file mode 100644
index 0000000..fe65f86
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.server.media.tests">
+
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
+
+    <application android:testOnly="true" android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.media.tests"
+         android:label="Frameworks Services Tests"/>
+</manifest>
diff --git a/services/tests/media/mediarouterservicetest/AndroidTest.xml b/services/tests/media/mediarouterservicetest/AndroidTest.xml
new file mode 100644
index 0000000..b065681
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs MediaRouter Service tests.">
+    <option name="test-tag" value="MediaRouterServiceTests" />
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="MediaRouterServiceTests.apk"/>
+        <option name="install-arg" value="-t" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.server.media.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
new file mode 100644
index 0000000..6f9b6fa
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
@@ -0,0 +1,341 @@
+/*
+ * 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.media;
+
+import static com.android.server.media.AudioRoutingUtils.ATTRIBUTES_MEDIA;
+import static com.android.server.media.AudioRoutingUtils.getMediaAudioProductStrategy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDevicePort;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.MediaRoute2Info;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class AudioPoliciesDeviceRouteControllerTest {
+
+    private static final String FAKE_ROUTE_NAME = "fake name";
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null);
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_WIRED_HEADSET, "name_wired_hs", /* address= */ null);
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "name_a2dp", /* address= */ "12:34:45");
+
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_EARPIECE, /* name= */ null, /* address= */ null);
+
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_NO_NAME =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET,
+                    /* name= */ null,
+                    /* address= */ null);
+
+    private AudioDeviceInfo mSelectedAudioDeviceInfo;
+    private Set<AudioDeviceInfo> mAvailableAudioDeviceInfos;
+    @Mock private AudioManager mMockAudioManager;
+    @Mock private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+    private AudioPoliciesDeviceRouteController mControllerUnderTest;
+    private AudioDeviceCallback mAudioDeviceCallback;
+    private AudioProductStrategy mMediaAudioProductStrategy;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Resources mockResources = Mockito.mock(Resources.class);
+        when(mockResources.getText(anyInt())).thenReturn(FAKE_ROUTE_NAME);
+        Context realContext = InstrumentationRegistry.getInstrumentation().getContext();
+        Context mockContext = Mockito.mock(Context.class);
+        when(mockContext.getResources()).thenReturn(mockResources);
+        // The bluetooth stack needs the application info, but we cannot use a spy because the
+        // concrete class is package private, so we just return the application info through the
+        // mock.
+        when(mockContext.getApplicationInfo()).thenReturn(realContext.getApplicationInfo());
+
+        // Setup the initial state so that the route controller is created in a sensible state.
+        mSelectedAudioDeviceInfo = FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER;
+        mAvailableAudioDeviceInfos = Set.of(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER);
+        updateMockAudioManagerState();
+        mMediaAudioProductStrategy = getMediaAudioProductStrategy();
+
+        BluetoothAdapter btAdapter =
+                realContext.getSystemService(BluetoothManager.class).getAdapter();
+        mControllerUnderTest =
+                new AudioPoliciesDeviceRouteController(
+                        mockContext,
+                        mMockAudioManager,
+                        Looper.getMainLooper(),
+                        mMediaAudioProductStrategy,
+                        btAdapter,
+                        mOnDeviceRouteChangedListener);
+        mControllerUnderTest.start(UserHandle.CURRENT_OR_SELF);
+
+        ArgumentCaptor<AudioDeviceCallback> deviceCallbackCaptor =
+                ArgumentCaptor.forClass(AudioDeviceCallback.class);
+        verify(mMockAudioManager)
+                .registerAudioDeviceCallback(deviceCallbackCaptor.capture(), any());
+        mAudioDeviceCallback = deviceCallbackCaptor.getValue();
+
+        // We clear any invocations during setup.
+        clearInvocations(mOnDeviceRouteChangedListener);
+    }
+
+    @Test
+    public void getSelectedRoute_afterDevicesConnect_returnsRightSelectedRoute() {
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged();
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+    }
+
+    @Test
+    public void getSelectedRoute_afterDeviceRemovals_returnsExpectedRoutes() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged();
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mOnDeviceRouteChangedListener, times(2)).onDeviceRouteChanged();
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        removeAvailableAudioDeviceInfos(
+                /* newSelectedDevice= */ null,
+                /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        removeAvailableAudioDeviceInfos(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER,
+                /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void onAudioDevicesAdded_clearsAudioRoutingPoliciesCorrectly() {
+        clearInvocations(mMockAudioManager);
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE);
+        verifyNoMoreInteractions(mMockAudioManager);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mMockAudioManager).removePreferredDeviceForStrategy(mMediaAudioProductStrategy);
+    }
+
+    @Test
+    public void getAvailableDevices_ignoresInvalidMediaOutputs() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE);
+        verifyNoMoreInteractions(mOnDeviceRouteChangedListener);
+        assertThat(
+                        mControllerUnderTest.getAvailableRoutes().stream()
+                                .map(MediaRoute2Info::getType)
+                                .toList())
+                .containsExactly(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void transferTo_setsTheExpectedRoutingPolicy() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        MediaRoute2Info builtInSpeakerRoute =
+                getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        mControllerUnderTest.transferTo(builtInSpeakerRoute.getId());
+        verify(mMockAudioManager)
+                .setPreferredDeviceForStrategy(
+                        mMediaAudioProductStrategy,
+                        createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER));
+
+        MediaRoute2Info wiredHeadsetRoute =
+                getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET);
+        mControllerUnderTest.transferTo(wiredHeadsetRoute.getId());
+        verify(mMockAudioManager)
+                .setPreferredDeviceForStrategy(
+                        mMediaAudioProductStrategy,
+                        createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET));
+    }
+
+    @Test
+    public void updateVolume_propagatesCorrectlyToRouteInfo() {
+        when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(2);
+        when(mMockAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(3);
+        when(mMockAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)).thenReturn(1);
+        when(mMockAudioManager.isVolumeFixed()).thenReturn(false);
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+
+        MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(selectedRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADSET);
+        assertThat(selectedRoute.getVolume()).isEqualTo(2);
+        assertThat(selectedRoute.getVolumeMax()).isEqualTo(3);
+        assertThat(selectedRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE);
+
+        MediaRoute2Info onlyTransferrableRoute =
+                mControllerUnderTest.getAvailableRoutes().stream()
+                        .filter(it -> !it.equals(selectedRoute))
+                        .findAny()
+                        .orElseThrow();
+        assertThat(onlyTransferrableRoute.getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolumeMax()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED);
+
+        when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(0);
+        when(mMockAudioManager.isVolumeFixed()).thenReturn(true);
+        mControllerUnderTest.updateVolume(0);
+        MediaRoute2Info newSelectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(newSelectedRoute.getVolume()).isEqualTo(0);
+        assertThat(newSelectedRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED);
+    }
+
+    @Test
+    public void getAvailableRoutes_whenNoProductNameIsProvided_usesTypeToPopulateName() {
+        assertThat(mControllerUnderTest.getSelectedRoute().getName().toString())
+                .isEqualTo(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER.getProductName().toString());
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_NO_NAME,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_NO_NAME);
+
+        MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(selectedRoute.getName().toString()).isEqualTo(FAKE_ROUTE_NAME);
+    }
+
+    // Internal methods.
+
+    @NonNull
+    private MediaRoute2Info getAvailableRouteWithType(int type) {
+        return mControllerUnderTest.getAvailableRoutes().stream()
+                .filter(it -> it.getType() == type)
+                .findFirst()
+                .orElseThrow();
+    }
+
+    private void addAvailableAudioDeviceInfo(
+            @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... newAvailableDevices) {
+        Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos);
+        newAvailableDeviceInfos.addAll(List.of(newAvailableDevices));
+        mAvailableAudioDeviceInfos = newAvailableDeviceInfos;
+        if (newSelectedDevice != null) {
+            mSelectedAudioDeviceInfo = newSelectedDevice;
+        }
+        updateMockAudioManagerState();
+        mAudioDeviceCallback.onAudioDevicesAdded(newAvailableDevices);
+    }
+
+    private void removeAvailableAudioDeviceInfos(
+            @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... devicesToRemove) {
+        Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos);
+        List.of(devicesToRemove).forEach(newAvailableDeviceInfos::remove);
+        mAvailableAudioDeviceInfos = newAvailableDeviceInfos;
+        if (newSelectedDevice != null) {
+            mSelectedAudioDeviceInfo = newSelectedDevice;
+        }
+        updateMockAudioManagerState();
+        mAudioDeviceCallback.onAudioDevicesRemoved(devicesToRemove);
+    }
+
+    private void updateMockAudioManagerState() {
+        when(mMockAudioManager.getDevicesForAttributes(ATTRIBUTES_MEDIA))
+                .thenReturn(
+                        List.of(createAudioDeviceAttribute(mSelectedAudioDeviceInfo.getType())));
+        when(mMockAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+                .thenReturn(mAvailableAudioDeviceInfos.toArray(new AudioDeviceInfo[0]));
+    }
+
+    private static AudioDeviceAttributes createAudioDeviceAttribute(int type) {
+        // Address is unused.
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_OUTPUT, type, /* address= */ "");
+    }
+
+    private static AudioDeviceInfo createAudioDeviceInfo(
+            int type, @NonNull String name, @NonNull String address) {
+        return new AudioDeviceInfo(AudioDevicePort.createForTesting(type, name, address));
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
index b3605cc..caa0864 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
@@ -221,6 +221,7 @@
         r.setStartUid(myUid());
         r.setHostingRecord(new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST));
         r.makeActive(thread, mAms.mProcessStats);
+        ProcessRecord.updateProcessRecordNodes(r);
         doNothing().when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(),
                 anyBoolean());
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index e4da2b6..cc6fc80 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -1756,6 +1756,7 @@
     private ProcessRecord makeProcessRecord(ApplicationInfo info) {
         final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
         r.setPid(mNextPid.incrementAndGet());
+        ProcessRecord.updateProcessRecordNodes(r);
         return r;
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 0fd424b..115a5b0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -336,6 +336,7 @@
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, processName, ai.uid));
         r.mState = spy(r.mState);
         r.setPid(mNextPid.getAndIncrement());
+        ProcessRecord.updateProcessRecordNodes(r);
         mActiveProcesses.add(r);
 
         final IApplicationThread thread;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index f45dd39..f386c3b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -114,7 +114,9 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -258,25 +260,40 @@
         ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
         lru.clear();
         Collections.addAll(lru, apps);
-        for (ProcessRecord app : apps) {
-            sService.mOomAdjuster.onProcessBeginLocked(app);
-        }
     }
 
     /**
      * Run updateOomAdjLocked().
+     * - If there is no process specified, run updateOomAdjLocked(int) on existing lru
      * - If there's only one process, then it calls updateOomAdjLocked(ProcessRecord, int).
      * - Otherwise, sets the processes to the LRU and run updateOomAdjLocked(int).
      */
     @SuppressWarnings("GuardedBy")
     private void updateOomAdj(ProcessRecord... apps) {
-        if (apps.length == 1) {
-            sService.mOomAdjuster.onProcessBeginLocked(apps[0]);
-            sService.mOomAdjuster.updateOomAdjLocked(apps[0], OOM_ADJ_REASON_NONE);
-        } else {
-            setProcessesToLru(apps);
+        if (apps.length == 0) {
+            updateProcessRecordNodes(sService.mProcessList.getLruProcessesLOSP());
             sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
-            sService.mProcessList.getLruProcessesLOSP().clear();
+        } else {
+            updateProcessRecordNodes(Arrays.asList(apps));
+            if (apps.length == 1) {
+                sService.mOomAdjuster.updateOomAdjLocked(apps[0], OOM_ADJ_REASON_NONE);
+            } else {
+                setProcessesToLru(apps);
+                sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+                sService.mProcessList.getLruProcessesLOSP().clear();
+            }
+        }
+    }
+
+    /**
+     * Fix up the pointers in the {@link ProcessRecordNode#mApp}:
+     * because we used the mokito spy objects all over the tests here, but the internal
+     * pointers in the {@link ProcessRecordNode#mApp} actually point to the real object.
+     * This needs to be fixed up here.
+     */
+    private void updateProcessRecordNodes(List<ProcessRecord> apps) {
+        for (ProcessRecord app : apps) {
+            ProcessRecord.updateProcessRecordNodes(app);
         }
     }
 
@@ -613,7 +630,6 @@
             s.lastTopAlmostPerceptibleBindRequestUptimeMs = nowUptime;
             s.getConnections().clear();
             app.mServices.updateHasTopStartedAlmostPerceptibleServices();
-            sService.mOomAdjuster.onProcessBeginLocked(system);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
             updateOomAdj(app);
 
@@ -636,7 +652,6 @@
             s.lastTopAlmostPerceptibleBindRequestUptimeMs =
                     nowUptime - 2 * sService.mConstants.mServiceBindAlmostPerceptibleTimeoutMs;
             app.mServices.updateHasTopStartedAlmostPerceptibleServices();
-            sService.mOomAdjuster.onProcessBeginLocked(system);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
             updateOomAdj(app);
 
@@ -660,7 +675,6 @@
                     nowUptime - 2 * sService.mConstants.mServiceBindAlmostPerceptibleTimeoutMs;
             s.getConnections().clear();
             app.mServices.updateHasTopStartedAlmostPerceptibleServices();
-            sService.mOomAdjuster.onProcessBeginLocked(system);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
             updateOomAdj(app);
 
@@ -679,7 +693,6 @@
                 MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, true));
         system.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         system.mState.setHasTopUi(true);
-        sService.mOomAdjuster.onProcessBeginLocked(system);
         // Simulate the system starting and binding to a service in the app.
         ServiceRecord s = bindService(app, system,
                 null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
@@ -1532,7 +1545,7 @@
 
         client2.mServices.setHasForegroundServices(false, 0, /* hasNoneType=*/false);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(client2, OOM_ADJ_REASON_NONE);
+        updateOomAdj(client2);
 
         assertEquals(PROCESS_STATE_CACHED_EMPTY, client2.mState.getSetProcState());
         assertEquals(PROCESS_STATE_CACHED_EMPTY, client.mState.getSetProcState());
@@ -2018,7 +2031,6 @@
         app.setPendingFinishAttach(true);
         app.mState.setHasForegroundActivities(false);
 
-        sService.mOomAdjuster.onProcessBeginLocked(app);
         sService.mOomAdjuster.setAttachingProcessStatesLSP(app);
         updateOomAdj(app);
 
@@ -2035,7 +2047,6 @@
         app.mState.setHasForegroundActivities(true);
         doReturn(app).when(sService).getTopApp();
 
-        sService.mOomAdjuster.onProcessBeginLocked(app);
         sService.mOomAdjuster.setAttachingProcessStatesLSP(app);
         updateOomAdj(app);
 
@@ -2106,7 +2117,7 @@
             setServiceMap(s3, MOCKAPP5_UID, cn3);
             setServiceMap(c2s, MOCKAPP3_UID, cn4);
             app2UidRecord.setIdle(false);
-            sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+            updateOomAdj();
 
             assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                     SCHED_GROUP_DEFAULT);
@@ -2470,8 +2481,6 @@
         lru.clear();
         lru.add(app2);
         lru.add(app);
-        sService.mOomAdjuster.onProcessBeginLocked(app2);
-        sService.mOomAdjuster.onProcessBeginLocked(app);
 
         final ComponentName cn = ComponentName.unflattenFromString(
                 MOCKAPP_PACKAGENAME + "/.TestService");
@@ -2506,7 +2515,7 @@
         app2.mState.setHasShownUi(false);
 
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+        updateOomAdj();
 
         assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-ui-services");
         assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj2, "cch-started-services");
@@ -2515,7 +2524,7 @@
         app.mState.setAdjType(null);
         app.mState.setSetAdj(UNKNOWN_ADJ);
         app.mState.setHasShownUi(false);
-        sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+        updateOomAdj();
 
         assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
 
@@ -2524,7 +2533,7 @@
         app.mState.setAdjType(null);
         app.mState.setSetAdj(UNKNOWN_ADJ);
         s.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1;
-        sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+        updateOomAdj();
 
         assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
 
@@ -2542,7 +2551,7 @@
         s.lastActivity = now;
 
         app.mServices.startService(s);
-        sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+        updateOomAdj();
 
         assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
         assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
@@ -2553,7 +2562,7 @@
         app.mState.setSetAdj(UNKNOWN_ADJ);
         app.mState.setHasShownUi(false);
         s.lastActivity = now - sService.mConstants.MAX_SERVICE_INACTIVITY - 1;
-        sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+        updateOomAdj();
 
         assertProcStates(app, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
         assertProcStates(app2, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
@@ -2561,7 +2570,7 @@
         doReturn(userOther).when(sService.mUserController).getCurrentUserId();
         sService.mOomAdjuster.handleUserSwitchedLocked();
 
-        sService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE);
+        updateOomAdj();
         assertProcStates(app, true, PROCESS_STATE_SERVICE, cachedAdj1, "cch-started-services");
         assertProcStates(app2, false, PROCESS_STATE_SERVICE, SERVICE_ADJ, "started-services");
     }
@@ -2584,7 +2593,7 @@
         app.mServices.updateHasAboveClientLocked();
         assertFalse(app.mServices.hasAboveClient());
 
-        sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index 3b83e3c..fc2e5b0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -40,6 +40,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
@@ -109,6 +110,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
 
 @RunWith(AndroidJUnit4.class)
@@ -126,6 +128,8 @@
 
     private MockitoSession mMockingSession;
     private String mPackageName;
+    private Map<String, Integer> mPackageCategories;
+    private Map<String, Integer> mPackageUids;
     private TestLooper mTestLooper;
     @Mock
     private PackageManager mMockPackageManager;
@@ -229,34 +233,50 @@
                 .strictness(Strictness.WARN)
                 .startMocking();
         mMockContext = new MockContext(InstrumentationRegistry.getContext());
+        mPackageCategories = new HashMap<>();
+        mPackageUids = new HashMap<>();
         mPackageName = mMockContext.getPackageName();
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
+        LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE);
+        mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
+    }
+
+    private void mockAppCategory(String packageName, int packageUid,
+            @ApplicationInfo.Category int category)
+            throws Exception {
+        reset(mMockPackageManager);
+        mPackageCategories.put(packageName, category);
+        mPackageUids.put(packageName, packageUid);
+        final List<PackageInfo> packageInfos = new ArrayList<>();
+        for (Map.Entry<String, Integer> entry : mPackageCategories.entrySet()) {
+
+            packageName = entry.getKey();
+            packageUid = mPackageUids.get(packageName);
+            category = entry.getValue();
+            ApplicationInfo applicationInfo = new ApplicationInfo();
+            applicationInfo.packageName = packageName;
+            applicationInfo.category = category;
+            when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), anyInt()))
+                    .thenReturn(applicationInfo);
+
+            final PackageInfo pi = new PackageInfo();
+            pi.packageName = packageName;
+            pi.applicationInfo = applicationInfo;
+            packageInfos.add(pi);
+
+            when(mMockPackageManager.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(
+                    packageUid);
+            when(mMockPackageManager.getPackagesForUid(packageUid)).thenReturn(
+                    new String[]{packageName});
+        }
+        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
+                .thenReturn(packageInfos);
         final Resources resources =
                 InstrumentationRegistry.getInstrumentation().getContext().getResources();
         when(mMockPackageManager.getResourcesForApplication(anyString()))
                 .thenReturn(resources);
-        when(mMockPackageManager.getPackageUidAsUser(mPackageName, USER_ID_1)).thenReturn(
-                DEFAULT_PACKAGE_UID);
-        LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
-
-        mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE);
-    }
-
-    private void mockAppCategory(String packageName, @ApplicationInfo.Category int category)
-            throws Exception {
-        reset(mMockPackageManager);
-        final ApplicationInfo gameApplicationInfo = new ApplicationInfo();
-        gameApplicationInfo.category = category;
-        gameApplicationInfo.packageName = packageName;
-        final PackageInfo pi = new PackageInfo();
-        pi.packageName = packageName;
-        pi.applicationInfo = gameApplicationInfo;
-        final List<PackageInfo> packages = new ArrayList<>();
-        packages.add(pi);
-        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
-                .thenReturn(packages);
-        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(gameApplicationInfo);
     }
 
     @After
@@ -508,7 +528,7 @@
 
     @Test
     public void testGetGameMode_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         mockModifyGameModeGranted();
         assertEquals(GameManager.GAME_MODE_UNSUPPORTED,
@@ -780,7 +800,7 @@
 
     @Test
     public void testDeviceConfig_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
         checkReportedAvailableGameModes(createServiceAndStartUser(USER_ID_1));
@@ -1588,7 +1608,7 @@
 
     @Test
     public void testSetGameState_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         mockDeviceConfigNone();
         mockModifyGameModeGranted();
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
@@ -1611,14 +1631,14 @@
         gameManagerService.addGameStateListener(mockListener);
         verify(binder).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
 
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_AUDIO);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_AUDIO);
         GameState gameState = new GameState(true, GameState.MODE_NONE);
         gameManagerService.setGameState(mPackageName, gameState, USER_ID_1);
         assertFalse(gameManagerService.mHandler.hasMessages(SET_GAME_STATE));
         mTestLooper.dispatchAll();
         verify(mockListener, never()).onGameStateChanged(anyString(), any(), anyInt());
 
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
         gameState = new GameState(true, GameState.MODE_NONE);
         gameManagerService.setGameState(mPackageName, gameState, USER_ID_1);
         assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE));
@@ -1654,7 +1674,7 @@
 
         gameManagerService.addGameStateListener(mockListener);
         gameManagerService.removeGameStateListener(mockListener);
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
         GameState gameState = new GameState(false, GameState.MODE_CONTENT);
         gameManagerService.setGameState(mPackageName, gameState, USER_ID_1);
         assertTrue(gameManagerService.mHandler.hasMessages(SET_GAME_STATE));
@@ -1670,13 +1690,17 @@
         return output;
     }
 
-    private void mockInterventionListForMultipleUsers() {
+    private void mockInterventionListForMultipleUsers() throws Exception {
         final String[] packageNames = new String[]{"com.android.app0",
                 "com.android.app1", "com.android.app2"};
+        int i = 1;
+        for (String p : packageNames) {
+            mockAppCategory(p, DEFAULT_PACKAGE_UID + i++, ApplicationInfo.CATEGORY_GAME);
+        }
 
         final ApplicationInfo[] applicationInfos = new ApplicationInfo[3];
         final PackageInfo[] pis = new PackageInfo[3];
-        for (int i = 0; i < 3; ++i) {
+        for (i = 0; i < 3; ++i) {
             applicationInfos[i] = new ApplicationInfo();
             applicationInfos[i].category = ApplicationInfo.CATEGORY_GAME;
             applicationInfos[i].packageName = packageNames[i];
@@ -1717,7 +1741,6 @@
                         new Injector());
         startUser(gameManagerService, USER_ID_1);
         startUser(gameManagerService, USER_ID_2);
-
         gameManagerService.setGameModeConfigOverride("com.android.app0", USER_ID_2,
                 GameManager.GAME_MODE_PERFORMANCE, "120", "0.6");
         gameManagerService.setGameModeConfigOverride("com.android.app2", USER_ID_2,
@@ -1953,7 +1976,7 @@
 
     @Test
     public void testUpdateCustomGameModeConfiguration_nonGame() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_IMAGE);
         mockModifyGameModeGranted();
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         gameManagerService.updateCustomGameModeConfiguration(mPackageName,
@@ -2013,13 +2036,14 @@
     }
 
     @Test
-    public void testWritingSettingFile_onShutdown() throws InterruptedException {
+    public void testWritingSettingFile_onShutdown() throws Exception {
         mockModifyGameModeGranted();
         mockDeviceConfigAll();
         GameManagerService gameManagerService = new GameManagerService(mMockContext);
         gameManagerService.onBootCompleted();
         startUser(gameManagerService, USER_ID_1);
         Thread.sleep(500);
+        mockAppCategory("com.android.app1", DEFAULT_PACKAGE_UID + 1, ApplicationInfo.CATEGORY_GAME);
         gameManagerService.setGameModeConfigOverride("com.android.app1", USER_ID_1,
                 GameManager.GAME_MODE_BATTERY, "60", "0.5");
         gameManagerService.setGameMode("com.android.app1", USER_ID_1,
@@ -2259,7 +2283,7 @@
         when(DeviceConfig.getProperty(anyString(), anyString()))
                 .thenReturn(configString);
         mockModifyGameModeGranted();
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_IMAGE);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
         assertEquals(GameManager.GAME_MODE_UNSUPPORTED,
@@ -2277,9 +2301,7 @@
         mockModifyGameModeGranted();
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         String someGamePkg = "some.game";
-        mockAppCategory(someGamePkg, ApplicationInfo.CATEGORY_GAME);
-        when(mMockPackageManager.getPackageUidAsUser(someGamePkg, USER_ID_1)).thenReturn(
-                DEFAULT_PACKAGE_UID + 1);
+        mockAppCategory(someGamePkg, DEFAULT_PACKAGE_UID + 1,  ApplicationInfo.CATEGORY_GAME);
         gameManagerService.setGameMode(someGamePkg, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
         assertEquals(GameManager.GAME_MODE_PERFORMANCE,
                 gameManagerService.getGameMode(someGamePkg, USER_ID_1));
@@ -2307,24 +2329,11 @@
     }
 
     @Test
-    public void testGamePowerMode_gamePackage() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
-        gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
-        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
-    }
-
-    @Test
     public void testGamePowerMode_twoGames() throws Exception {
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages1 = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
         String someGamePkg = "some.game";
-        String[] packages2 = {someGamePkg};
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
         HashMap<Integer, Boolean> powerState = new HashMap<>();
         doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1)))
                 .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean());
@@ -2333,6 +2342,7 @@
         assertTrue(powerState.get(Mode.GAME));
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        assertFalse(powerState.get(Mode.GAME));
         gameManagerService.mUidObserver.onUidStateChanged(
                 somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         assertTrue(powerState.get(Mode.GAME));
@@ -2344,12 +2354,9 @@
     @Test
     public void testGamePowerMode_twoGamesOverlap() throws Exception {
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages1 = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
         String someGamePkg = "some.game";
-        String[] packages2 = {someGamePkg};
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
@@ -2363,49 +2370,162 @@
     }
 
     @Test
-    public void testGamePowerMode_released() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
-        gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
-        gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
-        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
-    }
-
-    @Test
     public void testGamePowerMode_noPackage() throws Exception {
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
         String[] packages = {};
         when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
-        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
     }
 
     @Test
-    public void testGamePowerMode_notAGamePackage() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+    public void testGamePowerMode_gameAndNotGameApps_flagOn() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {"someapp"};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+
+        String nonGamePkg1 = "not.game1";
+        int nonGameUid1 = DEFAULT_PACKAGE_UID + 1;
+        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE);
+
+        String nonGamePkg2 = "not.game2";
+        int nonGameUid2 = DEFAULT_PACKAGE_UID + 2;
+        mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE);
+
+        String gamePkg1 = "game1";
+        int gameUid1 = DEFAULT_PACKAGE_UID + 3;
+        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME);
+
+        String gamePkg2 = "game2";
+        int gameUid2 = DEFAULT_PACKAGE_UID + 4;
+        mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME);
+
+        // non-game1 top and background with no-op
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
-        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game1 top to enable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game1 in foreground to disable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game2 in foreground with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid2, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game 2 in foreground with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid2, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game 1 in background with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game2 in background to resume game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid2, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game 1 in background with no-op
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game 2 in background to stop game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid2, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
     }
 
     @Test
-    public void testGamePowerMode_notAGamePackageNotReleased() throws Exception {
-        mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+    public void testGamePowerMode_gameAndNotGameApps_flagOff() throws Exception {
+        mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
         GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-        String[] packages = {"someapp"};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+
+        String nonGamePkg1 = "not.game1";
+        int nonGameUid1 = DEFAULT_PACKAGE_UID + 1;
+        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE);
+
+        String gamePkg1 = "game1";
+        int gameUid1 = DEFAULT_PACKAGE_UID + 3;
+        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME);
+
+        // non-game1 top and background with no-op
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
-                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
-        verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, false);
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // game1 top to enable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game1 in foreground to not interfere
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // non-game 1 in background to not interfere
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // move non-game1 to foreground again
+        gameManagerService.mUidObserver.onUidStateChanged(
+                nonGameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+
+        // with non-game1 on top, game 1 in background to still disable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
+
+        // with non-game1 on top, game 1 in foreground to still enable game mode
+        gameManagerService.mUidObserver.onUidStateChanged(
+                gameUid1, ActivityManager.PROCESS_STATE_TOP, 0, 0);
+        verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+        verify(mMockPowerManager, never()).setPowerMode(Mode.GAME, false);
+        clearInvocations(mMockPowerManager);
     }
 
     @Test
@@ -2432,8 +2552,6 @@
         gameManagerService.onBootCompleted();
 
         // Set up a game in the foreground.
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
 
@@ -2448,10 +2566,8 @@
 
         // Adding another game to the foreground.
         String anotherGamePkg = "another.game";
-        String[] packages2 = {anotherGamePkg};
-        mockAppCategory(anotherGamePkg, ApplicationInfo.CATEGORY_GAME);
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(anotherGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
 
         gameManagerService.mUidObserver.onUidStateChanged(
                 somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);
@@ -2484,8 +2600,6 @@
                         }));
 
         // Set up a game in the foreground.
-        String[] packages = {mPackageName};
-        when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
 
@@ -2503,10 +2617,8 @@
 
         // Toggle game default frame rate off.
         String anotherGamePkg = "another.game";
-        String[] packages2 = {anotherGamePkg};
-        mockAppCategory(anotherGamePkg, ApplicationInfo.CATEGORY_GAME);
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+        mockAppCategory(anotherGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
         gameManagerService.mUidObserver.onUidStateChanged(
                 somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index 293003d..32878b3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -67,6 +67,7 @@
 import android.location.LocationManagerInternal.ProviderEnabledListener;
 import android.location.LocationRequest;
 import android.location.LocationResult;
+import android.location.flags.Flags;
 import android.location.provider.IProviderRequestListener;
 import android.location.provider.ProviderProperties;
 import android.location.provider.ProviderRequest;
@@ -78,8 +79,10 @@
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.WorkSource;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.util.Log;
@@ -97,6 +100,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -140,6 +144,9 @@
     private static final WorkSource WORK_SOURCE = new WorkSource(IDENTITY.getUid());
     private static final String MISSING_PERMISSION = "missing_permission";
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private Random mRandom;
 
     @Mock
@@ -1347,6 +1354,24 @@
         assertThat(mManager.isVisibleToCaller()).isFalse();
     }
 
+    @Test
+    public void testValidateLocation_futureLocation() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_LOCATION_VALIDATION);
+        Location location = createLocation(NAME, mRandom);
+        mProvider.setProviderLocation(location);
+
+        assertThat(mPassive.getLastLocation(new LastLocationRequest.Builder().build(), IDENTITY,
+                PERMISSION_FINE)).isEqualTo(location);
+
+        Location futureLocation = createLocation(NAME, mRandom);
+        futureLocation.setElapsedRealtimeNanos(
+                SystemClock.elapsedRealtimeNanos() + TimeUnit.SECONDS.toNanos(2));
+        mProvider.setProviderLocation(futureLocation);
+
+        assertThat(mPassive.getLastLocation(new LastLocationRequest.Builder().build(), IDENTITY,
+                PERMISSION_FINE)).isEqualTo(location);
+    }
+
     @MediumTest
     @Test
     public void testEnableMsl_expectedBehavior() throws Exception {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
index c2b52b4..57326b2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -50,9 +50,10 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.pm.parsing.PackageParser2;
+import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.AndroidPackageLegacyUtils;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.pkg.AndroidPackage;
 
 import org.junit.Before;
@@ -175,7 +176,7 @@
                     mPmService.getPlatformPackage(), /* isUpdatedSystemApp */ false);
             // isUpdatedSystemApp is ignoreable above, only used for shared library adjustment
             return parsedPackage.hideAsFinal();
-        } catch (PackageManagerException e) {
+        } catch (PackageParserException e) {
             throw new RuntimeException(e);
         }
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index 7b29e2a..538c0ee 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -56,6 +56,7 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder
 import com.android.internal.R
+import com.android.internal.pm.parsing.PackageParser2
 import com.android.internal.pm.parsing.pkg.PackageImpl
 import com.android.internal.pm.parsing.pkg.ParsedPackage
 import com.android.internal.pm.pkg.parsing.ParsingPackage
@@ -69,7 +70,6 @@
 import com.android.server.extendedtestutils.wheneverStatic
 import com.android.server.pm.dex.DexManager
 import com.android.server.pm.dex.DynamicCodeLogger
-import com.android.server.pm.parsing.PackageParser2
 import com.android.server.pm.permission.PermissionManagerServiceInternal
 import com.android.server.pm.pkg.AndroidPackage
 import com.android.server.pm.resolution.ComponentResolver
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt
index da929af..7feafef 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceBootTest.kt
@@ -21,6 +21,7 @@
 import android.os.Build
 import android.os.Process
 import android.util.Log
+import com.android.internal.pm.parsing.PackageParserException
 import com.android.server.pm.pkg.AndroidPackage
 import com.android.server.testutils.whenever
 import java.io.File
@@ -120,7 +121,7 @@
                 argThat { path: File -> path.path.contains("a.data.package") },
                 anyInt(),
                 anyBoolean()))
-                .thenThrow(PackageManagerException(
+                .thenThrow(PackageParserException(
                         PackageManager.INSTALL_FAILED_INVALID_APK, "Oh no!"))
         val pm = createPackageManagerService()
         verify(rule.mocks().settings, Mockito.never()).insertPackageSettingLPw(
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
index ae53e70..7444403 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
@@ -19,6 +19,7 @@
 import android.app.AppOpsManager
 import android.content.Intent
 import android.content.pm.SuspendDialogInfo
+import android.content.pm.UserPackage
 import android.os.Binder
 import android.os.PersistableBundle
 import com.android.server.testutils.any
@@ -41,12 +42,18 @@
                 .thenReturn(AppOpsManager.MODE_DEFAULT)
     }
 
+    companion object {
+        val doUserPackage = UserPackage.of(TEST_USER_ID, DEVICE_OWNER_PACKAGE)
+        val platformUserPackage = UserPackage.of(TEST_USER_ID, PLATFORM_PACKAGE_NAME)
+        val testUserPackage1 = UserPackage.of(TEST_USER_ID, TEST_PACKAGE_1)
+    }
+
     @Test
     fun setPackagesSuspended() {
         val targetPackages = arrayOf(TEST_PACKAGE_1, TEST_PACKAGE_2)
         val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
         testHandler.flush()
 
@@ -63,14 +70,14 @@
     fun setPackagesSuspended_emptyPackageName() {
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 null /* packageNames */, true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
 
         assertThat(failedNames).isNull()
 
         failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOfNulls(0), true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
 
         assertThat(failedNames).isEmpty()
@@ -80,7 +87,8 @@
     fun setPackagesSuspended_callerIsNotAllowed() {
         val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(TEST_PACKAGE_2), true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, TEST_PACKAGE_1, TEST_USER_ID,
+                null /* launcherExtras */, null /* dialogInfo */,
+                testUserPackage1, TEST_USER_ID,
                 Binder.getCallingUid(), false /* quarantined */)
 
         assertThat(failedNames).asList().hasSize(1)
@@ -91,7 +99,7 @@
     fun setPackagesSuspended_callerSuspendItself() {
         val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(DEVICE_OWNER_PACKAGE), true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
 
         assertThat(failedNames).asList().hasSize(1)
@@ -102,7 +110,7 @@
     fun setPackagesSuspended_nonexistentPackage() {
         val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(NONEXISTENT_PACKAGE), true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
 
         assertThat(failedNames).asList().hasSize(1)
@@ -115,7 +123,7 @@
             INSTALLER_PACKAGE, UNINSTALLER_PACKAGE, VERIFIER_PACKAGE, PERMISSION_CONTROLLER_PACKAGE)
         val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 knownPackages, true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)!!
 
         assertThat(failedNames.size).isEqualTo(knownPackages.size)
@@ -129,14 +137,14 @@
         val targetPackages = arrayOf(TEST_PACKAGE_1, TEST_PACKAGE_2)
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
         testHandler.flush()
         Mockito.clearInvocations(broadcastHelper)
         assertThat(failedNames).isEmpty()
         failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, false /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
         testHandler.flush()
 
@@ -184,7 +192,7 @@
         appExtras.putString(TEST_PACKAGE_1, TEST_PACKAGE_1)
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(TEST_PACKAGE_1), true /* suspended */, appExtras, null /* launcherExtras */,
-                null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, doUserPackage, TEST_USER_ID, deviceOwnerUid,
                 false /* quarantined */)
         testHandler.flush()
         assertThat(failedNames).isEmpty()
@@ -202,22 +210,22 @@
         val targetPackages = arrayOf(TEST_PACKAGE_1, TEST_PACKAGE_2)
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, true /* suspended */, appExtras, null /* launcherExtras */,
-                null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, doUserPackage, TEST_USER_ID, deviceOwnerUid,
                 false /* quarantined */)
         testHandler.flush()
         Mockito.clearInvocations(broadcastHelper)
         assertThat(failedNames).isEmpty()
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-            TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
+            TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isEqualTo(doUserPackage)
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-            TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
+            TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(doUserPackage)
         assertThat(SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isNotNull()
         assertThat(SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isNotNull()
 
         suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(),
-            targetPackages, { suspendingPackage -> suspendingPackage == DEVICE_OWNER_PACKAGE },
+            targetPackages, { suspender -> suspender.packageName == DEVICE_OWNER_PACKAGE },
             TEST_USER_ID)
 
         testHandler.flush()
@@ -243,7 +251,7 @@
         launcherExtras.putString(TEST_PACKAGE_2, TEST_PACKAGE_2)
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(TEST_PACKAGE_2), true /* suspended */, null /* appExtras */, launcherExtras,
-                null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, doUserPackage, TEST_USER_ID, deviceOwnerUid,
                 false /* quarantined */)
         testHandler.flush()
         assertThat(failedNames).isEmpty()
@@ -258,7 +266,7 @@
     fun isPackageSuspended() {
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(TEST_PACKAGE_1), true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
+                null /* launcherExtras */, null /* dialogInfo */, doUserPackage,
                 TEST_USER_ID, deviceOwnerUid, false /* quarantined */)
         testHandler.flush()
         assertThat(failedNames).isEmpty()
@@ -273,13 +281,13 @@
         launcherExtras.putString(TEST_PACKAGE_2, TEST_PACKAGE_2)
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(TEST_PACKAGE_2), true /* suspended */, null /* appExtras */, launcherExtras,
-                null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, doUserPackage, TEST_USER_ID, deviceOwnerUid,
                 false /* quarantined */)
         testHandler.flush()
         assertThat(failedNames).isEmpty()
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-            TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
+            TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(doUserPackage)
     }
 
     @Test
@@ -290,57 +298,57 @@
         // Suspend.
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, true /* suspended */, null /* appExtras */, launcherExtras,
-                null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, doUserPackage, TEST_USER_ID, deviceOwnerUid,
                 false /* quarantined */)
         assertThat(failedNames).isEmpty()
         testHandler.flush()
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
+                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(doUserPackage)
 
         // Suspend by system.
         failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, true /* suspended */, null /* appExtras */, launcherExtras,
-                null /* dialogInfo */, PLATFORM_PACKAGE_NAME, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, platformUserPackage, TEST_USER_ID, deviceOwnerUid,
                 false /* quarantined */)
         assertThat(failedNames).isEmpty()
         testHandler.flush()
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(PLATFORM_PACKAGE_NAME)
+                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(platformUserPackage)
 
         // QAS by package1.
         failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, true /* suspended */, null /* appExtras */, launcherExtras,
-                null /* dialogInfo */, TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid,
+                null /* dialogInfo */, testUserPackage1, TEST_USER_ID, deviceOwnerUid,
                 true /* quarantined */)
         assertThat(failedNames).isEmpty()
         testHandler.flush()
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(TEST_PACKAGE_1)
+                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(testUserPackage1)
 
         // Un-QAS by package1.
         suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(),
-                targetPackages, { suspendingPackage -> suspendingPackage == TEST_PACKAGE_1 },
+                targetPackages, { suspendingPackage -> suspendingPackage == testUserPackage1 },
                 TEST_USER_ID)
         testHandler.flush()
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(PLATFORM_PACKAGE_NAME)
+                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(platformUserPackage)
 
         // Un-suspend by system.
         suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(),
-                targetPackages, { suspendingPackage -> suspendingPackage == PLATFORM_PACKAGE_NAME },
+                targetPackages, { suspender -> suspender.packageName == PLATFORM_PACKAGE_NAME },
                 TEST_USER_ID)
         testHandler.flush()
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
-                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
+                TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(doUserPackage)
 
         // Unsuspend.
         suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(),
-                targetPackages, { suspendingPackage -> suspendingPackage == DEVICE_OWNER_PACKAGE },
+                targetPackages, { suspendingPackage -> suspendingPackage == doUserPackage },
                 TEST_USER_ID)
         testHandler.flush()
 
@@ -354,13 +362,13 @@
             .setTitle(TEST_PACKAGE_1).build()
         var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 arrayOf(TEST_PACKAGE_1), true /* suspended */, null /* appExtras */,
-                null /* launcherExtras */, dialogInfo, DEVICE_OWNER_PACKAGE, TEST_USER_ID,
+                null /* launcherExtras */, dialogInfo, doUserPackage, TEST_USER_ID,
                 deviceOwnerUid, false /* quarantined */)
         testHandler.flush()
         assertThat(failedNames).isEmpty()
 
         val result = suspendPackageHelper.getSuspendedDialogInfo(pms.snapshotComputer(),
-            TEST_PACKAGE_1, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid)!!
+            TEST_PACKAGE_1, doUserPackage, TEST_USER_ID, deviceOwnerUid)!!
 
         assertThat(result.title).isEqualTo(TEST_PACKAGE_1)
     }
diff --git a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java b/services/tests/servicestests/src/com/android/server/BootReceiverTest.java
deleted file mode 100644
index 523c5c0..0000000
--- a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java
+++ /dev/null
@@ -1,97 +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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.test.AndroidTestCase;
-
-import com.android.server.os.TombstoneProtos;
-import com.android.server.os.TombstoneProtos.Tombstone;
-
-public class BootReceiverTest extends AndroidTestCase {
-    private static final String TAG = "BootReceiverTest";
-
-    public void testRemoveMemoryFromTombstone() {
-        Tombstone tombstoneBase = Tombstone.newBuilder()
-                .setBuildFingerprint("build_fingerprint")
-                .setRevision("revision")
-                .setPid(123)
-                .setTid(23)
-                .setUid(34)
-                .setSelinuxLabel("selinux_label")
-                .addCommandLine("cmd1")
-                .addCommandLine("cmd2")
-                .addCommandLine("cmd3")
-                .setProcessUptime(300)
-                .setAbortMessage("abort")
-                .addCauses(TombstoneProtos.Cause.newBuilder()
-                        .setHumanReadable("cause1")
-                        .setMemoryError(TombstoneProtos.MemoryError.newBuilder()
-                                .setTool(TombstoneProtos.MemoryError.Tool.SCUDO)
-                                .setType(TombstoneProtos.MemoryError.Type.DOUBLE_FREE)))
-                .addLogBuffers(TombstoneProtos.LogBuffer.newBuilder().setName("name").addLogs(
-                        TombstoneProtos.LogMessage.newBuilder()
-                                .setTimestamp("123")
-                                .setMessage("message")))
-                .addOpenFds(TombstoneProtos.FD.newBuilder().setFd(1).setPath("path"))
-                .build();
-
-        Tombstone tombstoneWithoutMemory = tombstoneBase.toBuilder()
-                .putThreads(1, TombstoneProtos.Thread.newBuilder()
-                        .setId(1)
-                        .setName("thread1")
-                        .addRegisters(TombstoneProtos.Register.newBuilder().setName("r1").setU64(1))
-                        .addRegisters(TombstoneProtos.Register.newBuilder().setName("r2").setU64(2))
-                        .addBacktraceNote("backtracenote1")
-                        .addUnreadableElfFiles("files1")
-                        .setTaggedAddrCtrl(1)
-                        .setPacEnabledKeys(10)
-                        .build())
-                .build();
-
-        Tombstone tombstoneWithMemory = tombstoneBase.toBuilder()
-                .addMemoryMappings(TombstoneProtos.MemoryMapping.newBuilder()
-                        .setBeginAddress(1)
-                        .setEndAddress(100)
-                        .setOffset(10)
-                        .setRead(true)
-                        .setWrite(true)
-                        .setExecute(false)
-                        .setMappingName("mapping")
-                        .setBuildId("build")
-                        .setLoadBias(70))
-                .putThreads(1, TombstoneProtos.Thread.newBuilder()
-                        .setId(1)
-                        .setName("thread1")
-                        .addRegisters(TombstoneProtos.Register.newBuilder().setName("r1").setU64(1))
-                        .addRegisters(TombstoneProtos.Register.newBuilder().setName("r2").setU64(2))
-                        .addBacktraceNote("backtracenote1")
-                        .addUnreadableElfFiles("files1")
-                        .addMemoryDump(TombstoneProtos.MemoryDump.newBuilder()
-                                .setRegisterName("register1")
-                                .setMappingName("mapping")
-                                .setBeginAddress(10))
-                        .setTaggedAddrCtrl(1)
-                        .setPacEnabledKeys(10)
-                        .build())
-                .build();
-
-        assertThat(BootReceiver.removeMemoryFromTombstone(tombstoneWithMemory))
-                .isEqualTo(tombstoneWithoutMemory);
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/OWNERS b/services/tests/servicestests/src/com/android/server/OWNERS
index 2d36ff3..d49bc43 100644
--- a/services/tests/servicestests/src/com/android/server/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/OWNERS
@@ -7,3 +7,4 @@
 per-file BatteryServiceTest.java = file:platform/hardware/interfaces:/health/OWNERS
 per-file GestureLauncherServiceTest.java = file:platform/packages/apps/EmergencyInfo:/OWNERS
 per-file PinnerServiceTest.java = file:/apct-tests/perftests/OWNERS
+per-file SecurityStateTest.java = file:/SECURITY_STATE_OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationPromptControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationPromptControllerTest.java
index 5fd28f57..332b1a2 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationPromptControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationPromptControllerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility.magnification;
 
+import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION;
 import static android.provider.Settings.Secure.ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT;
 
 import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_A11Y_WINDOW_MAGNIFICATION_FEATURE;
@@ -44,6 +45,7 @@
 import androidx.test.InstrumentationRegistry;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -70,6 +72,13 @@
     private WindowMagnificationPromptController mWindowMagnificationPromptController;
     private BroadcastReceiver mReceiver;
 
+    /**
+     *  return whether window magnification is supported for current test context.
+     */
+    private boolean isWindowModeSupported() {
+        return mTestableContext.getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION);
+    }
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -78,6 +87,9 @@
         setWindowMagnificationPromptSettings(true);
         mWindowMagnificationPromptController = new WindowMagnificationPromptController(
                 mTestableContext, TEST_USER);
+
+        // skip test if window magnification is not supported to prevent fail results.
+        Assume.assumeTrue(isWindowModeSupported());
     }
 
     @After
@@ -111,8 +123,8 @@
         final Intent intent = new Intent(ACTION_TURN_ON_IN_SETTINGS);
         mReceiver.onReceive(mTestableContext, intent);
 
-        assertThat(Settings.Secure.getInt(mResolver, ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT,
-                -1)).isEqualTo(0);
+        assertThat(Settings.Secure.getIntForUser(mResolver,
+                ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT, -1, TEST_USER)).isEqualTo(0);
         verify(mTestableContext.getSpyContext()).unregisterReceiver(mReceiver);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java
index 6fca561..69817ad 100644
--- a/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/FadeConfigurationsTest.java
@@ -16,17 +16,21 @@
 
 package com.android.server.audio;
 
-import static android.media.AudioAttributes.USAGE_MEDIA;
-import static android.media.AudioAttributes.USAGE_GAME;
-import static android.media.AudioAttributes.USAGE_ASSISTANT;
 import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
+import static android.media.AudioAttributes.USAGE_ASSISTANT;
+import static android.media.AudioAttributes.USAGE_EMERGENCY;
+import static android.media.AudioAttributes.USAGE_GAME;
+import static android.media.AudioAttributes.USAGE_MEDIA;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN;
+import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.media.AudioAttributes;
+import android.media.FadeManagerConfiguration;
 import android.media.VolumeShaper;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import com.google.common.truth.Expect;
 
@@ -42,6 +46,7 @@
 public final class FadeConfigurationsTest {
     private FadeConfigurations mFadeConfigurations;
     private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000;
+    private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000;
     private static final long DEFAULT_DELAY_FADE_IN_OFFENDERS_MS = 2000;
     private static final long DURATION_FOR_UNFADEABLE_MS = 0;
     private static final int TEST_UID_SYSTEM = 1000;
@@ -60,11 +65,19 @@
     private static final VolumeShaper.Configuration DEFAULT_FADEOUT_VSHAPE =
             new VolumeShaper.Configuration.Builder()
                     .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID)
-                    .setCurve(/* times= */new float[]{0.f, 0.25f, 1.0f} ,
-                            /* volumes= */new float[]{1.f, 0.65f, 0.0f})
+                    .setCurve(/* times= */ new float[]{0.f, 0.25f, 1.0f},
+                            /* volumes= */ new float[]{1.f, 0.65f, 0.0f})
                     .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
                     .setDuration(DEFAULT_FADE_OUT_DURATION_MS)
                     .build();
+    private static final VolumeShaper.Configuration DEFAULT_FADEIN_VSHAPE =
+            new VolumeShaper.Configuration.Builder()
+                    .setId(PlaybackActivityMonitor.VOLUME_SHAPER_SYSTEM_FADEOUT_ID)
+                    .setCurve(/* times= */ new float[]{0.f, 0.50f, 1.0f},
+                            /* volumes= */ new float[]{0.f, 0.30f, 1.0f})
+                    .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
+                    .setDuration(DEFAULT_FADE_IN_DURATION_MS)
+                    .build();
 
     private static final AudioAttributes TEST_MEDIA_AUDIO_ATTRIBUTE =
             new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build();
@@ -72,12 +85,18 @@
             new AudioAttributes.Builder().setUsage(USAGE_GAME).build();
     private static final AudioAttributes TEST_ASSISTANT_AUDIO_ATTRIBUTE =
             new AudioAttributes.Builder().setUsage(USAGE_ASSISTANT).build();
+    private static final AudioAttributes TEST_EMERGENCY_AUDIO_ATTRIBUTE =
+            new AudioAttributes.Builder().setSystemUsage(USAGE_EMERGENCY).build();
+
     private static final AudioAttributes TEST_SPEECH_AUDIO_ATTRIBUTE =
             new AudioAttributes.Builder().setContentType(CONTENT_TYPE_SPEECH).build();
 
     @Rule
     public final Expect expect = Expect.create();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() {
         mFadeConfigurations = new FadeConfigurations();
@@ -156,4 +175,110 @@
                 .that(mFadeConfigurations.isFadeable(TEST_GAME_AUDIO_ATTRIBUTE, TEST_UID_USER,
                         PLAYER_TYPE_AAUDIO)).isFalse();
     }
+
+    @Test
+    public void testGetFadeableUsages_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<Integer> usageList = List.of(AudioAttributes.USAGE_ALARM,
+                AudioAttributes.USAGE_EMERGENCY);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ usageList,
+                /* unfadeableContentTypes= */ null, /* unfadeableUids= */ null,
+                /* unfadeableAudioAttrs= */ null);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Fadeable usages with fade manager configuration")
+                .that(fadeConfigs.getFadeableUsages()).isEqualTo(fmc.getFadeableUsages());
+    }
+
+    @Test
+    public void testGetUnfadeableContentTypes_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<Integer> contentTypesList = List.of(AudioAttributes.CONTENT_TYPE_MUSIC,
+                AudioAttributes.CONTENT_TYPE_MOVIE);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null,
+                /* unfadeableContentTypes= */ contentTypesList, /* unfadeableUids= */ null,
+                /* unfadeableAudioAttrs= */ null);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Unfadeable content types with fade manager configuration")
+                .that(fadeConfigs.getUnfadeableContentTypes())
+                .isEqualTo(fmc.getUnfadeableContentTypes());
+    }
+
+    @Test
+    public void testGetUnfadeableAudioAttributes_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<AudioAttributes> attrsList = List.of(TEST_ASSISTANT_AUDIO_ATTRIBUTE,
+                TEST_EMERGENCY_AUDIO_ATTRIBUTE);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null,
+                /* unfadeableContentTypes= */ null, /* unfadeableUids= */ null,
+                /* unfadeableAudioAttrs= */ attrsList);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Unfadeable audio attributes with fade manager configuration")
+                .that(fadeConfigs.getUnfadeableAudioAttributes())
+                .isEqualTo(fmc.getUnfadeableAudioAttributes());
+    }
+
+    @Test
+    public void testGetUnfadeableUids_withFadeManagerConfigurations_equals() {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION);
+        List<Integer> uidsList = List.of(TEST_UID_SYSTEM, TEST_UID_USER);
+        FadeManagerConfiguration fmc = createFadeMgrConfig(/* fadeableUsages= */ null,
+                /* unfadeableContentTypes= */ null, /* unfadeableUids= */ uidsList,
+                /* unfadeableAudioAttrs= */ null);
+        FadeConfigurations fadeConfigs = new FadeConfigurations();
+
+        fadeConfigs.setFadeManagerConfiguration(fmc);
+
+        expect.withMessage("Unfadeable uids with fade manager configuration")
+                .that(fadeConfigs.getUnfadeableUids()).isEqualTo(fmc.getUnfadeableUids());
+    }
+
+    private static FadeManagerConfiguration createFadeMgrConfig(List<Integer> fadeableUsages,
+            List<Integer> unfadeableContentTypes, List<Integer> unfadeableUids,
+            List<AudioAttributes> unfadeableAudioAttrs) {
+        FadeManagerConfiguration.Builder builder = new FadeManagerConfiguration.Builder();
+        if (fadeableUsages != null) {
+            builder.setFadeableUsages(fadeableUsages);
+        }
+        if (unfadeableContentTypes != null) {
+            builder.setUnfadeableContentTypes(unfadeableContentTypes);
+        }
+        if (unfadeableUids != null) {
+            builder.setUnfadeableUids(unfadeableUids);
+        }
+        if (unfadeableAudioAttrs != null) {
+            builder.setUnfadeableAudioAttributes(unfadeableAudioAttrs);
+        }
+        if (fadeableUsages != null) {
+            for (int index = 0; index < fadeableUsages.size(); index++) {
+                builder.setFadeOutVolumeShaperConfigForAudioAttributes(
+                        createGenericAudioAttributesForUsage(fadeableUsages.get(index)),
+                        DEFAULT_FADEOUT_VSHAPE);
+            }
+        }
+        if (fadeableUsages != null) {
+            for (int index = 0; index < fadeableUsages.size(); index++) {
+                builder.setFadeInVolumeShaperConfigForAudioAttributes(
+                        createGenericAudioAttributesForUsage(fadeableUsages.get(index)),
+                        DEFAULT_FADEIN_VSHAPE);
+            }
+        }
+
+        return builder.build();
+    }
+
+    private static AudioAttributes createGenericAudioAttributesForUsage(int usage) {
+        if (AudioAttributes.isSystemUsage(usage)) {
+            return new AudioAttributes.Builder().setSystemUsage(usage).build();
+        }
+        return new AudioAttributes.Builder().setUsage(usage).build();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java
index 65059d5..fa94821 100644
--- a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java
@@ -23,8 +23,6 @@
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK;
 import static android.media.AudioPlaybackConfiguration.PLAYER_TYPE_UNKNOWN;
 
-import static org.junit.Assert.assertThrows;
-
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.media.AudioManager;
@@ -75,20 +73,11 @@
 
     @Before
     public void setUp() {
-        mFadeOutManager = new FadeOutManager(new FadeConfigurations());
+        mFadeOutManager = new FadeOutManager();
         mContext = ApplicationProvider.getApplicationContext();
     }
 
     @Test
-    public void constructor_nullFadeConfigurations_fails() {
-        Throwable thrown = assertThrows(NullPointerException.class, () -> new FadeOutManager(
-                /* FadeConfigurations= */ null));
-
-        expect.withMessage("Constructor exception")
-                .that(thrown).hasMessageThat().contains("Fade configurations can not be null");
-    }
-
-    @Test
     public void testCanCauseFadeOut_forFaders_returnsTrue() {
         FocusRequester winner = createFocusRequester(TEST_MEDIA_AUDIO_ATTRIBUTE, "winning-client",
                 "unit-test", TEST_UID_USER,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
index 3b5cae3..88b2ed4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
@@ -50,14 +50,22 @@
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.PromptInfo;
+import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.IFaceService;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintService;
 import android.hardware.iris.IIrisService;
 import android.os.Binder;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+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.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.InstrumentationRegistry;
@@ -89,6 +97,9 @@
 
     @Rule
     public MockitoRule mockitorule = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
@@ -118,6 +129,10 @@
     @Captor
     private ArgumentCaptor<List<FingerprintSensorPropertiesInternal>> mFingerprintPropsCaptor;
     @Captor
+    private ArgumentCaptor<FingerprintSensorConfigurations> mFingerprintSensorConfigurationsCaptor;
+    @Captor
+    private ArgumentCaptor<FaceSensorConfigurations> mFaceSensorConfigurationsCaptor;
+    @Captor
     private ArgumentCaptor<List<FaceSensorPropertiesInternal>> mFacePropsCaptor;
 
     @Before
@@ -143,6 +158,9 @@
         when(mContext.getResources()).thenReturn(mResources);
         when(mInjector.getBiometricService()).thenReturn(mBiometricService);
         when(mInjector.getConfiguration(any())).thenReturn(config);
+        when(mInjector.getFaceConfiguration(any())).thenReturn(config);
+        when(mInjector.getFingerprintConfiguration(any())).thenReturn(config);
+        when(mInjector.getIrisConfiguration(any())).thenReturn(config);
         when(mInjector.getFingerprintService()).thenReturn(mFingerprintService);
         when(mInjector.getFaceService()).thenReturn(mFaceService);
         when(mInjector.getIrisService()).thenReturn(mIrisService);
@@ -173,12 +191,13 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(com.android.server.biometrics.Flags.FLAG_DE_HIDL)
     public void testRegisterAuthenticator_registerAuthenticators() throws Exception {
         final int fingerprintId = 0;
         final int fingerprintStrength = 15;
 
         final int faceId = 1;
-        final int faceStrength = 4095;
+        final int faceStrength = 15;
 
         final String[] config = {
                 // ID0:Fingerprint:Strong
@@ -206,6 +225,51 @@
                 Utils.authenticatorStrengthToPropertyStrength(faceStrength));
     }
 
+    @Test
+    @RequiresFlagsEnabled(com.android.server.biometrics.Flags.FLAG_DE_HIDL)
+    public void testRegisterAuthenticator_registerAuthenticatorsLegacy() throws RemoteException {
+        final int fingerprintId = 0;
+        final int fingerprintStrength = 15;
+
+        final int faceId = 1;
+        final int faceStrength = 4095;
+
+        final String[] config = {
+                // ID0:Fingerprint:Strong
+                String.format("%d:2:%d", fingerprintId, fingerprintStrength),
+                // ID2:Face:Convenience
+                String.format("%d:8:%d", faceId, faceStrength)
+        };
+
+        when(mInjector.getFingerprintConfiguration(any())).thenReturn(config);
+        when(mInjector.getFaceConfiguration(any())).thenReturn(config);
+        when(mInjector.getFingerprintAidlInstances()).thenReturn(new String[]{});
+        when(mInjector.getFaceAidlInstances()).thenReturn(new String[]{});
+
+        mAuthService = new AuthService(mContext, mInjector);
+        mAuthService.onStart();
+
+        verify(mFingerprintService).registerAuthenticatorsLegacy(
+                mFingerprintSensorConfigurationsCaptor.capture());
+
+        final SensorProps[] fingerprintProp = mFingerprintSensorConfigurationsCaptor.getValue()
+                .getSensorPairForInstance("defaultHIDL").second;
+
+        assertEquals(fingerprintProp[0].commonProps.sensorId, fingerprintId);
+        assertEquals(fingerprintProp[0].commonProps.sensorStrength,
+                Utils.authenticatorStrengthToPropertyStrength(fingerprintStrength));
+
+        verify(mFaceService).registerAuthenticatorsLegacy(
+                mFaceSensorConfigurationsCaptor.capture());
+
+        final android.hardware.biometrics.face.SensorProps[] faceProp =
+                mFaceSensorConfigurationsCaptor.getValue()
+                        .getSensorPairForInstance("defaultHIDL").second;
+
+        assertEquals(faceProp[0].commonProps.sensorId, faceId);
+        assertEquals(faceProp[0].commonProps.sensorStrength,
+                Utils.authenticatorStrengthToPropertyStrength(faceStrength));
+    }
 
     // TODO(b/141025588): Check that an exception is thrown when the userId != callingUserId
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
index 94cb860..74f8f08 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
@@ -29,7 +29,7 @@
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.filters.SmallTest;
 
@@ -43,10 +43,10 @@
 import java.util.ArrayList;
 import java.util.List;
 
-@RequiresFlagsDisabled(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
 @Presubmit
 @SmallTest
 public class SensorOverlaysTest {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private static final int SENSOR_ID = 11;
     private static final long REQUEST_ID = 8;
@@ -59,6 +59,7 @@
 
     @Before
     public void setup() {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
         when(mAcquisitionClient.getRequestId()).thenReturn(REQUEST_ID);
         when(mAcquisitionClient.hasRequestId()).thenReturn(true);
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
new file mode 100644
index 0000000..3aaac2e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.biometrics.sensors.face;
+
+import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
+import static android.hardware.face.FaceSensorProperties.TYPE_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.SensorProps;
+import android.hardware.face.FaceAuthenticateOptions;
+import android.hardware.face.FaceSensorConfigurations;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
+import android.hardware.face.IFaceServiceReceiver;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+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.TestableContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.R;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.biometrics.Flags;
+import com.android.server.biometrics.Utils;
+import com.android.server.biometrics.sensors.face.aidl.FaceProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+@SmallTest
+public class FaceServiceTest {
+    private static final int ID_DEFAULT = 2;
+    private static final int ID_VIRTUAL = 6;
+    private static final String NAME_DEFAULT = "default";
+    private static final String NAME_VIRTUAL = "virtual";
+    private static final String OP_PACKAGE_NAME = "FaceServiceTest/SystemUi";
+
+    @Rule
+    public final MockitoRule mMockito = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
+    @Rule
+    public final FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule();
+    @Mock
+    private FaceProvider mFaceProviderDefault;
+    @Mock
+    private FaceProvider mFaceProviderVirtual;
+    @Mock
+    private IFace mDefaultFaceDaemon;
+    @Mock
+    private IFace mVirtualFaceDaemon;
+    @Mock
+    private IBiometricService mIBiometricService;
+    @Mock
+    private IBinder mToken;
+    @Mock
+    private IFaceServiceReceiver mFaceServiceReceiver;
+
+    private final SensorProps mDefaultSensorProps = new SensorProps();
+    private final SensorProps mVirtualSensorProps = new SensorProps();
+    private FaceService mFaceService;
+    private final FaceSensorPropertiesInternal mSensorPropsDefault =
+            new FaceSensorPropertiesInternal(ID_DEFAULT, STRENGTH_STRONG,
+                    2 /* maxEnrollmentsPerUser */,
+                    List.of(),
+                    TYPE_UNKNOWN,
+                    true /* supportsFaceDetection */,
+                    true /* supportsSelfIllumination */,
+                    false /* resetLockoutRequiresChallenge */);
+    private final FaceSensorPropertiesInternal mSensorPropsVirtual =
+            new FaceSensorPropertiesInternal(ID_VIRTUAL, STRENGTH_STRONG,
+                    2 /* maxEnrollmentsPerUser */,
+                    List.of(),
+                    TYPE_UNKNOWN,
+                    true /* supportsFaceDetection */,
+                    true /* supportsSelfIllumination */,
+                    false /* resetLockoutRequiresChallenge */);
+    private FaceSensorConfigurations mFaceSensorConfigurations;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mDefaultFaceDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mDefaultSensorProps});
+        when(mVirtualFaceDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mVirtualSensorProps});
+        when(mFaceProviderDefault.getSensorProperties()).thenReturn(List.of(mSensorPropsDefault));
+        when(mFaceProviderVirtual.getSensorProperties()).thenReturn(List.of(mSensorPropsVirtual));
+        when(mFaceProviderDefault.containsSensor(anyInt()))
+                .thenAnswer(i -> i.getArguments()[0].equals(ID_DEFAULT));
+        when(mFaceProviderVirtual.containsSensor(anyInt()))
+                .thenAnswer(i -> i.getArguments()[0].equals(ID_VIRTUAL));
+
+        mContext.getTestablePermissions().setPermission(
+                USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_GRANTED);
+        mFaceSensorConfigurations = new FaceSensorConfigurations(false);
+        mFaceSensorConfigurations.addAidlConfigs(new String[]{NAME_DEFAULT, NAME_VIRTUAL},
+                (name) -> {
+                    if (name.equals(IFace.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                        return mDefaultFaceDaemon;
+                    } else if (name.equals(IFace.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                        return mVirtualFaceDaemon;
+                    }
+                    return null;
+                });
+    }
+
+    private void initService() {
+        mFaceService = new FaceService(mContext,
+                (filteredSensorProps, resetLockoutRequiresChallenge) -> {
+                    if (NAME_DEFAULT.equals(filteredSensorProps.first)) return mFaceProviderDefault;
+                    if (NAME_VIRTUAL.equals(filteredSensorProps.first)) return mFaceProviderVirtual;
+                    return null;
+                }, () -> mIBiometricService,
+                (name) -> {
+                    if (NAME_DEFAULT.equals(name)) return mFaceProviderDefault;
+                    if (NAME_VIRTUAL.equals(name)) return mFaceProviderVirtual;
+                    return null;
+                },
+                () -> new String[]{NAME_DEFAULT, NAME_VIRTUAL});
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_defaultOnly() throws Exception {
+        initService();
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)),
+                any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualOnly() throws Exception {
+        initService();
+        Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
+                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1);
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception {
+        mFaceSensorConfigurations = new FaceSensorConfigurations(false);
+        mFaceSensorConfigurations.addAidlConfigs(new String[]{NAME_VIRTUAL},
+                (name) -> {
+                    if (name.equals(IFace.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                        return mDefaultFaceDaemon;
+                    } else if (name.equals(IFace.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                        return mVirtualFaceDaemon;
+                    }
+                    return null;
+                });
+        initService();
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any());
+    }
+
+    @Test
+    public void testOptionsForAuthentication() throws Exception {
+        FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
+                .build();
+        initService();
+        mFaceService.mServiceWrapper.registerAuthenticators(List.of());
+        waitForRegistration();
+
+        final long operationId = 5;
+        mFaceService.mServiceWrapper.authenticate(mToken, operationId, mFaceServiceReceiver,
+                faceAuthenticateOptions);
+
+        assertThat(faceAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT);
+    }
+
+    @Test
+    public void testOptionsForDetect() throws Exception {
+        FaceAuthenticateOptions faceAuthenticateOptions = new FaceAuthenticateOptions.Builder()
+                .setOpPackageName(ComponentName.unflattenFromString(OP_PACKAGE_NAME)
+                        .getPackageName())
+                .build();
+        mContext.getOrCreateTestableResources().addOverride(
+                R.string.config_keyguardComponent,
+                OP_PACKAGE_NAME);
+        initService();
+        mFaceService.mServiceWrapper.registerAuthenticators(List.of());
+        waitForRegistration();
+        mFaceService.mServiceWrapper.detectFace(mToken, mFaceServiceReceiver,
+                faceAuthenticateOptions);
+
+        assertThat(faceAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT);
+    }
+
+    private void waitForRegistration() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mFaceService.mServiceWrapper.addAuthenticatorsRegisteredCallback(
+                new IFaceAuthenticatorsRegisteredCallback.Stub() {
+                    public void onAllAuthenticatorsRegistered(
+                            List<FaceSensorPropertiesInternal> sensors) {
+                        latch.countDown();
+                    }
+                });
+        latch.await(10, TimeUnit.SECONDS);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index f43120d..8b1a291 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.biometrics.sensors.face.aidl;
 
+import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -32,15 +35,19 @@
 import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.SensorProps;
+import android.hardware.face.HidlFaceSensorConfig;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
@@ -49,6 +56,7 @@
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -59,6 +67,10 @@
 @SmallTest
 public class FaceProviderTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String TAG = "FaceProviderTest";
 
     private static final float FRR_THRESHOLD = 0.2f;
@@ -109,7 +121,7 @@
 
         mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
                 mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext,
-                mDaemon);
+                mDaemon, false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */);
     }
 
     @Test
@@ -124,10 +136,38 @@
 
             assertThat(currentClient).isInstanceOf(FaceInternalCleanupClient.class);
             assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId);
-            assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+            assertThat(currentClient.getTargetUserId()).isEqualTo(USER_SYSTEM);
         }
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testAddingHidlSensors() {
+        when(mResources.getIntArray(anyInt())).thenReturn(new int[]{});
+        when(mResources.getBoolean(anyInt())).thenReturn(false);
+
+        final int faceId = 0;
+        final int faceStrength = 15;
+        final String config = String.format("%d:8:%d", faceId, faceStrength);
+        final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig();
+        faceSensorConfig.parse(config, mContext);
+        final HidlFaceSensorConfig[] hidlFaceSensorConfig =
+                new HidlFaceSensorConfig[]{faceSensorConfig};
+        mFaceProvider = new FaceProvider(mContext,
+                mBiometricStateCallback, hidlFaceSensorConfig, TAG,
+                mLockoutResetDispatcher, mBiometricContext, mDaemon,
+                true /* resetLockoutRequiresChallenge */,
+                true /* testHalEnabled */);
+
+        assertThat(mFaceProvider.mFaceSensors.get(faceId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_NULL);
+
+        waitForIdle();
+
+        assertThat(mFaceProvider.mFaceSensors.get(faceId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM);
+    }
+
     @SuppressWarnings("rawtypes")
     @Test
     public void halServiceDied_resetsAllSchedulers() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
index 7a293e8..e7f7195 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
@@ -157,7 +157,7 @@
                 sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
         final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext,
                 null /* handler */, internalProp, mLockoutResetDispatcher, mBiometricContext);
-
+        sensor.init(mLockoutResetDispatcher, mFaceProvider);
         mScheduler.reset();
 
         assertNull(mScheduler.getCurrentClient());
@@ -185,6 +185,7 @@
                 sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
         final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null,
                 internalProp, mLockoutResetDispatcher, mBiometricContext, mCurrentSession);
+        sensor.init(mLockoutResetDispatcher, mFaceProvider);
         mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
         sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
                 USER_ID, mHalCallback);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java
new file mode 100644
index 0000000..4e43332
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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.biometrics.sensors.face.hidl;
+
+import static com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.face.V1_0.IBiometricsFace;
+import android.hardware.biometrics.face.V1_0.OptionalUint64;
+import android.hardware.biometrics.face.V1_0.Status;
+import android.hardware.face.Face;
+import android.hardware.face.HidlFaceSensorConfig;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.testing.TestableContext;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricUtils;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.face.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.face.aidl.FaceEnrollClient;
+import com.android.server.biometrics.sensors.face.aidl.FaceProvider;
+import com.android.server.biometrics.sensors.face.aidl.FaceResetLockoutClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class HidlToAidlSensorAdapterTest {
+    private static final String TAG = "HidlToAidlSensorAdapterTest";
+    private static final int USER_ID = 2;
+    private static final int SENSOR_ID = 4;
+    private static final byte[] HAT = new byte[69];
+    @Rule
+    public final MockitoRule mMockito = MockitoJUnit.rule();
+
+    @Mock
+    private IBiometricService mBiometricService;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForSensor;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForClient;
+    @Mock
+    private BiometricLogger mLogger;
+    @Mock
+    private BiometricContext mBiometricContext;
+    @Mock
+    private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private FaceProvider mFaceProvider;
+    @Mock
+    private Runnable mInternalCleanupAndGetFeatureRunnable;
+    @Mock
+    private IBiometricsFace mDaemon;
+    @Mock
+    AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    @Mock
+    BiometricUtils<Face> mBiometricUtils;
+
+    private final TestLooper mLooper = new TestLooper();
+    private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter;
+    private final TestableContext mContext = new TestableContext(
+            ApplicationProvider.getApplicationContext());
+
+    @Before
+    public void setUp() throws RemoteException {
+        final OptionalUint64 result = new OptionalUint64();
+        result.status = Status.OK;
+
+        when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+        when(mDaemon.setCallback(any())).thenReturn(result);
+        doAnswer((answer) -> {
+            mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                    .onLockoutChanged(0);
+            return null;
+        }).when(mDaemon).resetLockout(any());
+        doAnswer((answer) -> {
+            mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                    .onEnrollmentProgress(1, 0);
+            return null;
+        }).when(mDaemon).enroll(any(), anyInt(), any());
+
+        mContext.getOrCreateTestableResources();
+
+        final String config = String.format("%d:8:15", SENSOR_ID);
+        final BiometricScheduler scheduler = new BiometricScheduler(TAG,
+                new Handler(mLooper.getLooper()),
+                BiometricScheduler.SENSOR_TYPE_FACE,
+                null /* gestureAvailabilityTracker */,
+                mBiometricService, 10 /* recentOperationsLimit */);
+        final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig();
+        faceSensorConfig.parse(config, mContext);
+        mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG, mFaceProvider,
+                mContext, new Handler(mLooper.getLooper()), faceSensorConfig,
+                mLockoutResetDispatcherForSensor, mBiometricContext,
+                false /* resetLockoutRequiresChallenge */, mInternalCleanupAndGetFeatureRunnable,
+                mAuthSessionCoordinator, mDaemon, mAidlResponseHandlerCallback);
+        mHidlToAidlSensorAdapter.init(mLockoutResetDispatcherForSensor, mFaceProvider);
+        mHidlToAidlSensorAdapter.setScheduler(scheduler);
+        mHidlToAidlSensorAdapter.handleUserChanged(USER_ID);
+    }
+
+    @Test
+    public void lockoutTimedResetViaClient() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FaceResetLockoutClient(mContext, mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false/* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mLockoutResetDispatcherForClient, never()).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutTimedResetViaCallback() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutChanged(0);
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID))
+                .isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaCallback() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutChanged(0);
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaClient() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FaceResetLockoutClient(mContext,
+                        mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForClient, never()).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator, never()).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void verifyOnEnrollSuccessCallback() {
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(new FaceEnrollClient(mContext,
+                mHidlToAidlSensorAdapter.getLazySession(), null /* token */, null /* listener */,
+                USER_ID, HAT, TAG, 1 /* requestId */, mBiometricUtils,
+                new int[]{} /* disabledFeatures */, ENROLL_TIMEOUT_SEC, null /* previewSurface */,
+                SENSOR_ID, mLogger, mBiometricContext, 1 /* maxTemplatesPerUser */,
+                false /* debugConsent */));
+        mLooper.dispatchAll();
+
+        verify(mAidlResponseHandlerCallback).onEnrollSuccess();
+    }
+
+    private void setLockoutTimed() {
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                .onLockoutChanged(1);
+        mLooper.dispatchAll();
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID))
+                .isEqualTo(LockoutTracker.LOCKOUT_TIMED);
+    }
+
+    private void setLockoutPermanent() {
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                .onLockoutChanged(-1);
+        mLooper.dispatchAll();
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_PERMANENT);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java
similarity index 80%
rename from services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java
rename to services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java
index 9a40e8a..b9a4fb4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/AidlToHidlAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapterTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.biometrics.sensors.face.hidl;
 
-import static com.android.server.biometrics.sensors.face.hidl.AidlToHidlAdapter.ENROLL_TIMEOUT_SEC;
+import static com.android.server.biometrics.sensors.face.hidl.HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC;
 import static com.android.server.biometrics.sensors.face.hidl.FaceGenerateChallengeClient.CHALLENGE_TIMEOUT_SEC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -68,7 +68,7 @@
 
 @Presubmit
 @SmallTest
-public class AidlToHidlAdapterTest {
+public class HidlToAidlSessionAdapterTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -84,25 +84,32 @@
     private Clock mClock;
 
     private final long mChallenge = 100L;
-    private AidlToHidlAdapter mAidlToHidlAdapter;
+    private HidlToAidlSessionAdapter mHidlToAidlSessionAdapter;
     private final Face mFace = new Face("face" /* name */, 1 /* faceId */, 0 /* deviceId */);
     private final int mFeature = BiometricFaceConstants.FEATURE_REQUIRE_REQUIRE_DIVERSITY;
     private final byte[] mFeatures = new byte[]{Feature.REQUIRE_ATTENTION};
 
     @Before
     public void setUp() throws RemoteException {
+        final OptionalUint64 setCallbackResult = new OptionalUint64();
+        setCallbackResult.value = 1;
+
+        when(mSession.setCallback(any())).thenReturn(setCallbackResult);
+
         TestableContext testableContext = new TestableContext(
                 InstrumentationRegistry.getInstrumentation().getContext());
         testableContext.addMockSystemService(FaceManager.class, mFaceManager);
-        mAidlToHidlAdapter = new AidlToHidlAdapter(testableContext, () -> mSession, 0 /* userId */,
-                mAidlResponseHandler, mClock);
+
+        mHidlToAidlSessionAdapter = new HidlToAidlSessionAdapter(testableContext, () -> mSession,
+                0 /* userId */, mAidlResponseHandler, mClock);
         mHardwareAuthToken.timestamp = new Timestamp();
         mHardwareAuthToken.mac = new byte[10];
-        final OptionalUint64 result = new OptionalUint64();
-        result.status = Status.OK;
-        result.value = mChallenge;
 
-        when(mSession.generateChallenge(anyInt())).thenReturn(result);
+        final OptionalUint64 generateChallengeResult = new OptionalUint64();
+        generateChallengeResult.status = Status.OK;
+        generateChallengeResult.value = mChallenge;
+
+        when(mSession.generateChallenge(anyInt())).thenReturn(generateChallengeResult);
         when(mFaceManager.getEnrolledFaces(anyInt())).thenReturn(List.of(mFace));
     }
 
@@ -112,16 +119,16 @@
 
         final ArgumentCaptor<Long> challengeCaptor = ArgumentCaptor.forClass(Long.class);
 
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         verify(mSession).generateChallenge(CHALLENGE_TIMEOUT_SEC);
         verify(mAidlResponseHandler).onChallengeGenerated(challengeCaptor.capture());
         assertThat(challengeCaptor.getValue()).isEqualTo(mChallenge);
 
         forwardTime(10 /* seconds */);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
         forwardTime(20 /* seconds */);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         //Confirms that the challenge is cached and the hal method is not called again
         verifyNoMoreInteractions(mSession);
@@ -129,7 +136,7 @@
                 .onChallengeGenerated(mChallenge);
 
         forwardTime(60 /* seconds */);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         //HAL method called after challenge has timed out
         verify(mSession, times(2)).generateChallenge(CHALLENGE_TIMEOUT_SEC);
@@ -138,11 +145,11 @@
     @Test
     public void testRevokeChallenge_waitsUntilEmpty() throws RemoteException {
         for (int i = 0; i < 3; i++) {
-            mAidlToHidlAdapter.generateChallenge();
+            mHidlToAidlSessionAdapter.generateChallenge();
             forwardTime(10 /* seconds */);
         }
         for (int i = 0; i < 3; i++) {
-            mAidlToHidlAdapter.revokeChallenge(0);
+            mHidlToAidlSessionAdapter.revokeChallenge(0);
             forwardTime((i + 1) * 10 /* seconds */);
         }
 
@@ -151,20 +158,19 @@
 
     @Test
     public void testRevokeChallenge_timeout() throws RemoteException {
-        mAidlToHidlAdapter.generateChallenge();
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
         forwardTime(700);
-        mAidlToHidlAdapter.generateChallenge();
-        mAidlToHidlAdapter.revokeChallenge(0);
+        mHidlToAidlSessionAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.revokeChallenge(0);
 
         verify(mSession).revokeChallenge();
     }
 
     @Test
     public void testEnroll() throws RemoteException {
-        ICancellationSignal cancellationSignal = mAidlToHidlAdapter.enroll(mHardwareAuthToken,
-                EnrollmentType.DEFAULT, mFeatures,
-                null /* previewSurface */);
+        ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.enroll(
+                mHardwareAuthToken, EnrollmentType.DEFAULT, mFeatures, null /* previewSurface */);
         ArgumentCaptor<ArrayList<Integer>> featureCaptor = ArgumentCaptor.forClass(ArrayList.class);
 
         verify(mSession).enroll(any(), eq(ENROLL_TIMEOUT_SEC), featureCaptor.capture());
@@ -182,7 +188,8 @@
     @Test
     public void testAuthenticate() throws RemoteException {
         final int operationId = 2;
-        ICancellationSignal cancellationSignal = mAidlToHidlAdapter.authenticate(operationId);
+        ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.authenticate(
+                operationId);
 
         verify(mSession).authenticate(operationId);
 
@@ -193,7 +200,7 @@
 
     @Test
     public void testDetectInteraction() throws RemoteException {
-        ICancellationSignal cancellationSignal = mAidlToHidlAdapter.detectInteraction();
+        ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.detectInteraction();
 
         verify(mSession).authenticate(0);
 
@@ -204,7 +211,7 @@
 
     @Test
     public void testEnumerateEnrollments() throws RemoteException {
-        mAidlToHidlAdapter.enumerateEnrollments();
+        mHidlToAidlSessionAdapter.enumerateEnrollments();
 
         verify(mSession).enumerate();
     }
@@ -212,7 +219,7 @@
     @Test
     public void testRemoveEnrollment() throws RemoteException {
         final int[] enrollments = new int[]{1};
-        mAidlToHidlAdapter.removeEnrollments(enrollments);
+        mHidlToAidlSessionAdapter.removeEnrollments(enrollments);
 
         verify(mSession).remove(enrollments[0]);
     }
@@ -226,8 +233,8 @@
 
         when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result);
 
-        mAidlToHidlAdapter.setFeature(mFeature);
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.setFeature(mFeature);
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler).onFeaturesRetrieved(featureRetrieved.capture());
@@ -244,8 +251,8 @@
 
         when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result);
 
-        mAidlToHidlAdapter.setFeature(mFeature);
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.setFeature(mFeature);
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler).onFeaturesRetrieved(featureRetrieved.capture());
@@ -260,8 +267,8 @@
 
         when(mSession.getFeature(eq(mFeature), anyInt())).thenReturn(result);
 
-        mAidlToHidlAdapter.setFeature(mFeature);
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.setFeature(mFeature);
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler, never()).onFeaturesRetrieved(any());
@@ -270,7 +277,7 @@
 
     @Test
     public void testGetFeatures_featureNotSet() throws RemoteException {
-        mAidlToHidlAdapter.getFeatures();
+        mHidlToAidlSessionAdapter.getFeatures();
 
         verify(mSession, never()).getFeature(eq(mFeature), anyInt());
         verify(mAidlResponseHandler, never()).onFeaturesRetrieved(any());
@@ -283,7 +290,7 @@
 
         when(mSession.setFeature(anyInt(), anyBoolean(), any(), anyInt())).thenReturn(Status.OK);
 
-        mAidlToHidlAdapter.setFeature(mHardwareAuthToken, feature, enabled);
+        mHidlToAidlSessionAdapter.setFeature(mHardwareAuthToken, feature, enabled);
 
         verify(mAidlResponseHandler).onFeatureSet(feature);
     }
@@ -296,7 +303,7 @@
         when(mSession.setFeature(anyInt(), anyBoolean(), any(), anyInt()))
                 .thenReturn(Status.INTERNAL_ERROR);
 
-        mAidlToHidlAdapter.setFeature(mHardwareAuthToken, feature, enabled);
+        mHidlToAidlSessionAdapter.setFeature(mHardwareAuthToken, feature, enabled);
 
         verify(mAidlResponseHandler).onError(BiometricFaceConstants.FACE_ERROR_UNKNOWN,
                 0 /* vendorCode */);
@@ -311,7 +318,7 @@
 
         when(mSession.getAuthenticatorId()).thenReturn(result);
 
-        mAidlToHidlAdapter.getAuthenticatorId();
+        mHidlToAidlSessionAdapter.getAuthenticatorId();
 
         verify(mSession).getAuthenticatorId();
         verify(mAidlResponseHandler).onAuthenticatorIdRetrieved(authenticatorId);
@@ -319,7 +326,7 @@
 
     @Test
     public void testResetLockout() throws RemoteException {
-        mAidlToHidlAdapter.resetLockout(mHardwareAuthToken);
+        mHidlToAidlSessionAdapter.resetLockout(mHardwareAuthToken);
 
         ArgumentCaptor<ArrayList> hatCaptor = ArgumentCaptor.forClass(ArrayList.class);
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
index 2aa62d9..88956b6 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
@@ -40,24 +40,33 @@
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
+import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.SensorProps;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
+import android.hardware.fingerprint.FingerprintSensorConfigurations;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.os.Binder;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+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.TestableContext;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.R;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.server.LocalServices;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
@@ -85,10 +94,14 @@
     private static final String NAME_VIRTUAL = "virtual";
     private static final List<FingerprintSensorPropertiesInternal> HIDL_AUTHENTICATORS =
             List.of();
+    private static final String OP_PACKAGE_NAME = "FingerprintServiceTest/SystemUi";
 
     @Rule
     public final MockitoRule mMockito = MockitoJUnit.rule();
     @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
     public final TestableContext mContext = new TestableContext(
             InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
     @Rule
@@ -110,6 +123,10 @@
     private IBinder mToken;
     @Mock
     private VirtualDeviceManagerInternal mVdmInternal;
+    @Mock
+    private IFingerprint mDefaultFingerprintDaemon;
+    @Mock
+    private IFingerprint mVirtualFingerprintDaemon;
 
     @Captor
     private ArgumentCaptor<FingerprintAuthenticateOptions> mAuthenticateOptionsCaptor;
@@ -126,7 +143,10 @@
                     List.of(),
                     TYPE_UDFPS_OPTICAL,
                     false /* resetLockoutRequiresHardwareAuthToken */);
+    private FingerprintSensorConfigurations mFingerprintSensorConfigurations;
     private FingerprintService mService;
+    private final SensorProps mDefaultSensorProps = new SensorProps();
+    private final SensorProps mVirtualSensorProps = new SensorProps();
 
     @Before
     public void setup() throws Exception {
@@ -139,6 +159,10 @@
                 .thenAnswer(i -> i.getArguments()[0].equals(ID_DEFAULT));
         when(mFingerprintVirtual.containsSensor(anyInt()))
                 .thenAnswer(i -> i.getArguments()[0].equals(ID_VIRTUAL));
+        when(mDefaultFingerprintDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mDefaultSensorProps});
+        when(mVirtualFingerprintDaemon.getSensorProps()).thenReturn(
+                new SensorProps[]{mVirtualSensorProps});
 
         mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
         for (int permission : List.of(OP_USE_BIOMETRIC, OP_USE_FINGERPRINT)) {
@@ -150,6 +174,18 @@
             mContext.getTestablePermissions().setPermission(
                     permission, PackageManager.PERMISSION_GRANTED);
         }
+
+        mFingerprintSensorConfigurations = new FingerprintSensorConfigurations(
+                true /* resetLockoutRequiresHardwareAuthToken */);
+        mFingerprintSensorConfigurations.addAidlSensors(new String[]{NAME_DEFAULT, NAME_VIRTUAL},
+                (name) -> {
+                    if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                        return mDefaultFingerprintDaemon;
+                    } else if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                        return mVirtualFingerprintDaemon;
+                    }
+                    return null;
+                });
     }
 
     private void initServiceWith(String... aidlInstances) {
@@ -160,6 +196,10 @@
                     if (NAME_DEFAULT.equals(name)) return mFingerprintDefault;
                     if (NAME_VIRTUAL.equals(name)) return mFingerprintVirtual;
                     return null;
+                }, (sensorPropsPair, resetLockoutRequiresHardwareAuthToken) -> {
+                    if (NAME_DEFAULT.equals(sensorPropsPair.first)) return mFingerprintDefault;
+                    if (NAME_VIRTUAL.equals(sensorPropsPair.first)) return mFingerprintVirtual;
+                    return null;
                 });
     }
 
@@ -180,6 +220,17 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_defaultOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
+
+        mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any());
+    }
+
+    @Test
     public void registerAuthenticators_virtualOnly() throws Exception {
         initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
         Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
@@ -192,6 +243,19 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
+        Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
+                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1);
+
+        mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
+    }
+
+    @Test
     public void registerAuthenticators_virtualAlwaysWhenNoOther() throws Exception {
         initServiceWith(NAME_VIRTUAL);
 
@@ -201,6 +265,28 @@
         verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception {
+        mFingerprintSensorConfigurations =
+                new FingerprintSensorConfigurations(true);
+        mFingerprintSensorConfigurations.addAidlSensors(new String[]{NAME_VIRTUAL},
+                        (name) -> {
+                            if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_DEFAULT)) {
+                                return mDefaultFingerprintDaemon;
+                            } else if (name.equals(IFingerprint.DESCRIPTOR + "/" + NAME_VIRTUAL)) {
+                                return mVirtualFingerprintDaemon;
+                            }
+                            return null;
+                        });
+        initServiceWith(NAME_VIRTUAL);
+
+        mService.mServiceWrapper.registerAuthenticatorsLegacy(mFingerprintSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
+    }
+
     private void waitForRegistration() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         mService.mServiceWrapper.addAuthenticatorsRegisteredCallback(
@@ -260,6 +346,24 @@
         assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid());
     }
 
+    @Test
+    public void testOptionsForDetect() throws Exception {
+        FingerprintAuthenticateOptions fingerprintAuthenticateOptions =
+                new FingerprintAuthenticateOptions.Builder()
+                        .setOpPackageName(ComponentName.unflattenFromString(
+                                OP_PACKAGE_NAME).getPackageName())
+                        .build();
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.string.config_keyguardComponent,
+                OP_PACKAGE_NAME);
+        initServiceWithAndWait(NAME_DEFAULT);
+        mService.mServiceWrapper.detectFingerprint(mToken, mServiceReceiver,
+                fingerprintAuthenticateOptions);
+
+        assertThat(fingerprintAuthenticateOptions.getSensorId()).isEqualTo(ID_DEFAULT);
+    }
+
 
     private FingerprintAuthenticateOptions verifyAuthenticateWithNewRequestId(
             FingerprintProvider provider, long operationId) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index c24227f..774ea5b 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -161,6 +161,7 @@
 
     @Before
     public void setup() {
+        mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR);
         mContext.addMockSystemService(BiometricManager.class, mBiometricManager);
         when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
         when(mBiometricLogger.getAmbientLightProbe(anyBoolean())).thenAnswer(i ->
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index 4cfb83f..bf5986c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -34,17 +37,20 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.SensorLocation;
 import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.fingerprint.HidlFingerprintSensorConfig;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.AuthenticationStateListeners;
-import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.HalClientMonitor;
@@ -52,6 +58,7 @@
 import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -64,6 +71,10 @@
 
     private static final String TAG = "FingerprintProviderTest";
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock
     private Context mContext;
     @Mock
@@ -115,7 +126,8 @@
         mFingerprintProvider = new FingerprintProvider(mContext,
                 mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG,
                 mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext,
-                mDaemon);
+                mDaemon, false /* resetLockoutRequiresHardwareAuthToken */,
+                true /* testHalEnabled */);
     }
 
     @Test
@@ -123,17 +135,42 @@
         waitForIdle();
 
         for (SensorProps prop : mSensorProps) {
-            final BiometricScheduler scheduler =
-                    mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId)
-                            .getScheduler();
-            BaseClientMonitor currentClient = scheduler.getCurrentClient();
-
-            assertThat(currentClient).isInstanceOf(FingerprintInternalCleanupClient.class);
-            assertThat(currentClient.getSensorId()).isEqualTo(prop.commonProps.sensorId);
-            assertThat(currentClient.getTargetUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+            final Sensor sensor =
+                    mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId);
+            assertThat(sensor.getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM);
         }
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
+    public void testAddingHidlSensors() {
+        when(mResources.getIntArray(anyInt())).thenReturn(new int[]{});
+        when(mResources.getBoolean(anyInt())).thenReturn(false);
+
+        final int fingerprintId = 0;
+        final int fingerprintStrength = 15;
+        final String config = String.format("%d:2:%d", fingerprintId, fingerprintStrength);
+        final HidlFingerprintSensorConfig fingerprintSensorConfig =
+                new HidlFingerprintSensorConfig();
+        fingerprintSensorConfig.parse(config, mContext);
+        HidlFingerprintSensorConfig[] hidlFingerprintSensorConfigs =
+                new HidlFingerprintSensorConfig[]{fingerprintSensorConfig};
+        mFingerprintProvider = new FingerprintProvider(mContext,
+                mBiometricStateCallback, mAuthenticationStateListeners,
+                hidlFingerprintSensorConfigs, TAG, mLockoutResetDispatcher,
+                mGestureAvailabilityDispatcher, mBiometricContext, mDaemon,
+                false /* resetLockoutRequiresHardwareAuthToken */,
+                true /* testHalEnabled */);
+
+        assertThat(mFingerprintProvider.mFingerprintSensors.get(fingerprintId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_NULL);
+
+        waitForIdle();
+
+        assertThat(mFingerprintProvider.mFingerprintSensors.get(fingerprintId)
+                .getLazySession().get().getUserId()).isEqualTo(USER_SYSTEM);
+    }
+
     @SuppressWarnings("rawtypes")
     @Test
     public void halServiceDied_resetsAllSchedulers() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
index 4102600..126a05e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
@@ -96,7 +96,7 @@
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
 
-    private UserAwareBiometricScheduler mScheduler;
+    private BiometricScheduler mScheduler;
     private AidlResponseHandler mHalCallback;
 
     @Before
@@ -164,7 +164,8 @@
         final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext,
                 null /* handler */, internalProp, mLockoutResetDispatcher,
                 mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession);
-        mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
+        sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher);
+        mScheduler = sensor.getScheduler();
         sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
                 USER_ID, mHalCallback);
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
new file mode 100644
index 0000000..89a4961
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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.biometrics.sensors.fingerprint.hidl;
+
+import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.HidlFingerprintSensorConfig;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.testing.TestableContext;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricUtils;
+import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.LockoutResetDispatcher;
+import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.StartUserClient;
+import com.android.server.biometrics.sensors.StopUserClient;
+import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler;
+import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintEnrollClient;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
+import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@Presubmit
+@SmallTest
+public class HidlToAidlSensorAdapterTest {
+
+    private static final String TAG = "HidlToAidlSensorAdapterTest";
+    private static final int USER_ID = 2;
+    private static final int SENSOR_ID = 4;
+    private static final byte[] HAT = new byte[69];
+
+    @Rule
+    public final MockitoRule mMockito = MockitoJUnit.rule();
+
+    @Mock
+    private IBiometricService mBiometricService;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForSensor;
+    @Mock
+    private LockoutResetDispatcher mLockoutResetDispatcherForClient;
+    @Mock
+    private BiometricLogger mLogger;
+    @Mock
+    private BiometricContext mBiometricContext;
+    @Mock
+    private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private FingerprintProvider mFingerprintProvider;
+    @Mock
+    private GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
+    @Mock
+    private Runnable mInternalCleanupRunnable;
+    @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
+    private IBiometricsFingerprint mDaemon;
+    @Mock
+    private AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback;
+    @Mock
+    private BiometricUtils<Fingerprint> mBiometricUtils;
+    @Mock
+    private AuthenticationStateListeners mAuthenticationStateListeners;
+
+    private final TestLooper mLooper = new TestLooper();
+    private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter;
+    private final TestableContext mContext = new TestableContext(
+            ApplicationProvider.getApplicationContext());
+
+    private final UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback =
+            new UserAwareBiometricScheduler.UserSwitchCallback() {
+                @NonNull
+                @Override
+                public StopUserClient<?> getStopUserClient(int userId) {
+                    return new StopUserClient<IBiometricsFingerprint>(mContext,
+                            mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null, USER_ID,
+                            SENSOR_ID, mLogger, mBiometricContext, () -> {}) {
+                        @Override
+                        protected void startHalOperation() {
+                            getCallback().onClientFinished(this, true /* success */);
+                        }
+
+                        @Override
+                        public void unableToStart() {}
+                    };
+                }
+
+                @NonNull
+                @Override
+                public StartUserClient<?, ?> getStartUserClient(int newUserId) {
+                    return new StartUserClient<IBiometricsFingerprint, AidlSession>(mContext,
+                            mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null,
+                            USER_ID, SENSOR_ID,
+                            mLogger, mBiometricContext,
+                            (newUserId1, newUser, halInterfaceVersion) ->
+                                    mHidlToAidlSensorAdapter.handleUserChanged(newUserId1)) {
+                        @Override
+                        public void start(@NonNull ClientMonitorCallback callback) {
+                            super.start(callback);
+                            startHalOperation();
+                        }
+
+                        @Override
+                        protected void startHalOperation() {
+                            mUserStartedCallback.onUserStarted(USER_ID, null, 0);
+                            getCallback().onClientFinished(this, true /* success */);
+                        }
+
+                        @Override
+                        public void unableToStart() {}
+                    };
+                }
+            };;
+
+    @Before
+    public void setUp() throws RemoteException {
+        when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
+        doAnswer((answer) -> {
+            mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback()
+                    .onEnrollmentProgress(1 /* enrollmentId */, 0 /* remaining */);
+            return null;
+        }).when(mDaemon).enroll(any(), anyInt(), anyInt());
+
+        mContext.addMockSystemService(AlarmManager.class, mAlarmManager);
+        mContext.getOrCreateTestableResources();
+
+        final String config = String.format("%d:2:15", SENSOR_ID);
+        final UserAwareBiometricScheduler scheduler = new UserAwareBiometricScheduler(TAG,
+                new Handler(mLooper.getLooper()),
+                BiometricScheduler.SENSOR_TYPE_FP_OTHER,
+                null /* gestureAvailabilityDispatcher */,
+                mBiometricService,
+                () -> USER_ID,
+                mUserSwitchCallback);
+        final HidlFingerprintSensorConfig fingerprintSensorConfig =
+                new HidlFingerprintSensorConfig();
+        fingerprintSensorConfig.parse(config, mContext);
+        mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG,
+                mFingerprintProvider, mContext, new Handler(mLooper.getLooper()),
+                fingerprintSensorConfig, mLockoutResetDispatcherForSensor,
+                mGestureAvailabilityDispatcher, mBiometricContext,
+                false /* resetLockoutRequiresHardwareAuthToken */,
+                mInternalCleanupRunnable, mAuthSessionCoordinator, mDaemon,
+                mAidlResponseHandlerCallback);
+        mHidlToAidlSensorAdapter.init(mGestureAvailabilityDispatcher,
+                mLockoutResetDispatcherForSensor);
+        mHidlToAidlSensorAdapter.setScheduler(scheduler);
+        mHidlToAidlSensorAdapter.handleUserChanged(USER_ID);
+    }
+
+    @Test
+    public void lockoutTimedResetViaClient() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FingerprintResetLockoutClient(mContext,
+                        mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        verify(mAlarmManager).setExact(anyInt(), anyLong(), any());
+        verify(mLockoutResetDispatcherForClient).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutTimedResetViaCallback() {
+        setLockoutTimed();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutCleared();
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor, times(2)).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaCallback() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback().onLockoutCleared();
+        mLooper.dispatchAll();
+
+        verify(mLockoutResetDispatcherForSensor, times(2)).notifyLockoutResetCallbacks(eq(
+                SENSOR_ID));
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void lockoutPermanentResetViaClient() {
+        setLockoutPermanent();
+
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(
+                new FingerprintResetLockoutClient(mContext,
+                        mHidlToAidlSensorAdapter.getLazySession(),
+                        USER_ID, TAG, SENSOR_ID, mLogger, mBiometricContext, HAT,
+                        mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */),
+                        mLockoutResetDispatcherForClient, 0 /* biometricStrength */));
+        mLooper.dispatchAll();
+
+        verify(mAlarmManager, atLeast(1)).setExact(anyInt(), anyLong(), any());
+        verify(mLockoutResetDispatcherForClient).notifyLockoutResetCallbacks(SENSOR_ID);
+        verify(mLockoutResetDispatcherForSensor).notifyLockoutResetCallbacks(SENSOR_ID);
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_NONE);
+        verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong());
+    }
+
+    @Test
+    public void verifyOnEnrollSuccessCallback() {
+        mHidlToAidlSensorAdapter.getScheduler().scheduleClientMonitor(new FingerprintEnrollClient(
+                mContext, mHidlToAidlSensorAdapter.getLazySession(), null /* token */,
+                1 /* requestId */, null /* listener */, USER_ID, HAT, TAG, mBiometricUtils,
+                SENSOR_ID, mLogger, mBiometricContext,
+                mHidlToAidlSensorAdapter.getSensorProperties(), null, null,
+                mAuthenticationStateListeners, 5 /* maxTemplatesPerUser */, ENROLL_ENROLL));
+        mLooper.dispatchAll();
+
+        verify(mAidlResponseHandlerCallback).onEnrollSuccess();
+    }
+
+    private void setLockoutPermanent() {
+        for (int i = 0; i < 20; i++) {
+            mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                    .addFailedAttemptForUser(USER_ID);
+        }
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_PERMANENT);
+    }
+
+    private void setLockoutTimed() {
+        for (int i = 0; i < 5; i++) {
+            mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                    .addFailedAttemptForUser(USER_ID);
+        }
+
+        assertThat(mHidlToAidlSensorAdapter.getLockoutTracker(false /* forAuth */)
+                .getLockoutModeForUser(USER_ID)).isEqualTo(LockoutTracker.LOCKOUT_TIMED);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java
similarity index 79%
rename from services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java
rename to services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java
index b78ba82..d723e87 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/AidlToHidlAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapterTest.java
@@ -41,7 +41,7 @@
 
 @Presubmit
 @SmallTest
-public class AidlToHidlAdapterTest {
+public class HidlToAidlSessionAdapterTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -54,11 +54,11 @@
 
     private final long mChallenge = 100L;
     private final int mUserId = 0;
-    private AidlToHidlAdapter mAidlToHidlAdapter;
+    private HidlToAidlSessionAdapter mHidlToAidlSessionAdapter;
 
     @Before
     public void setUp() {
-        mAidlToHidlAdapter = new AidlToHidlAdapter(() -> mSession, mUserId,
+        mHidlToAidlSessionAdapter = new HidlToAidlSessionAdapter(() -> mSession, mUserId,
                 mAidlResponseHandler);
         mHardwareAuthToken.timestamp = new Timestamp();
         mHardwareAuthToken.mac = new byte[10];
@@ -67,7 +67,7 @@
     @Test
     public void testGenerateChallenge() throws RemoteException {
         when(mSession.preEnroll()).thenReturn(mChallenge);
-        mAidlToHidlAdapter.generateChallenge();
+        mHidlToAidlSessionAdapter.generateChallenge();
 
         verify(mSession).preEnroll();
         verify(mAidlResponseHandler).onChallengeGenerated(mChallenge);
@@ -75,7 +75,7 @@
 
     @Test
     public void testRevokeChallenge() throws RemoteException {
-        mAidlToHidlAdapter.revokeChallenge(mChallenge);
+        mHidlToAidlSessionAdapter.revokeChallenge(mChallenge);
 
         verify(mSession).postEnroll();
         verify(mAidlResponseHandler).onChallengeRevoked(0L);
@@ -84,9 +84,9 @@
     @Test
     public void testEnroll() throws RemoteException {
         final ICancellationSignal cancellationSignal =
-                mAidlToHidlAdapter.enroll(mHardwareAuthToken);
+                mHidlToAidlSessionAdapter.enroll(mHardwareAuthToken);
 
-        verify(mSession).enroll(any(), anyInt(), eq(AidlToHidlAdapter.ENROLL_TIMEOUT_SEC));
+        verify(mSession).enroll(any(), anyInt(), eq(HidlToAidlSessionAdapter.ENROLL_TIMEOUT_SEC));
 
         cancellationSignal.cancel();
 
@@ -96,7 +96,8 @@
     @Test
     public void testAuthenticate() throws RemoteException {
         final int operationId = 2;
-        final ICancellationSignal cancellationSignal = mAidlToHidlAdapter.authenticate(operationId);
+        final ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter.authenticate(
+                operationId);
 
         verify(mSession).authenticate(operationId, mUserId);
 
@@ -107,7 +108,8 @@
 
     @Test
     public void testDetectInteraction() throws RemoteException {
-        final ICancellationSignal cancellationSignal = mAidlToHidlAdapter.detectInteraction();
+        final ICancellationSignal cancellationSignal = mHidlToAidlSessionAdapter
+                .detectInteraction();
 
         verify(mSession).authenticate(0 /* operationId */, mUserId);
 
@@ -118,7 +120,7 @@
 
     @Test
     public void testEnumerateEnrollments() throws RemoteException {
-        mAidlToHidlAdapter.enumerateEnrollments();
+        mHidlToAidlSessionAdapter.enumerateEnrollments();
 
         verify(mSession).enumerate();
     }
@@ -126,7 +128,7 @@
     @Test
     public void testRemoveEnrollment() throws RemoteException {
         final int[] enrollmentIds = new int[]{1};
-        mAidlToHidlAdapter.removeEnrollments(enrollmentIds);
+        mHidlToAidlSessionAdapter.removeEnrollments(enrollmentIds);
 
         verify(mSession).remove(mUserId, enrollmentIds[0]);
     }
@@ -134,14 +136,14 @@
     @Test
     public void testRemoveMultipleEnrollments() throws RemoteException {
         final int[] enrollmentIds = new int[]{1, 2};
-        mAidlToHidlAdapter.removeEnrollments(enrollmentIds);
+        mHidlToAidlSessionAdapter.removeEnrollments(enrollmentIds);
 
         verify(mSession).remove(mUserId, 0);
     }
 
     @Test
     public void testResetLockout() throws RemoteException {
-        mAidlToHidlAdapter.resetLockout(mHardwareAuthToken);
+        mHidlToAidlSessionAdapter.resetLockout(mHardwareAuthToken);
 
         verify(mAidlResponseHandler).onLockoutCleared();
     }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
index edfe1b4..9b28b81 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -24,10 +24,8 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.companion.virtual.camera.VirtualCameraCallback;
 import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraMetadata;
 import android.companion.virtual.camera.VirtualCameraStreamConfig;
 import android.companion.virtualcamera.IVirtualCameraService;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
@@ -55,11 +53,11 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class VirtualCameraControllerTest {
 
-    private static final int CAMERA_DISPLAY_NAME_RES_ID_1 = 10;
+    private static final String CAMERA_NAME_1 = "Virtual camera 1";
     private static final int CAMERA_WIDTH_1 = 100;
     private static final int CAMERA_HEIGHT_1 = 200;
 
-    private static final int CAMERA_DISPLAY_NAME_RES_ID_2 = 11;
+    private static final String CAMERA_NAME_2 = "Virtual camera 2";
     private static final int CAMERA_WIDTH_2 = 400;
     private static final int CAMERA_HEIGHT_2 = 600;
     private static final int CAMERA_FORMAT = ImageFormat.YUV_420_888;
@@ -86,7 +84,7 @@
     @Test
     public void registerCamera_registersCamera() throws Exception {
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_1));
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
 
         ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
                 ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
@@ -100,7 +98,7 @@
     @Test
     public void unregisterCamera_unregistersCamera() throws Exception {
         VirtualCameraConfig config = createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_1);
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1);
         mVirtualCameraController.registerCamera(config);
 
         mVirtualCameraController.unregisterCamera(config);
@@ -111,9 +109,9 @@
     @Test
     public void close_unregistersAllCameras() throws Exception {
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_1));
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_2));
+                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_NAME_2));
 
         mVirtualCameraController.close();
 
@@ -131,10 +129,10 @@
     }
 
     private VirtualCameraConfig createVirtualCameraConfig(
-            int width, int height, int format, int displayNameResId) {
+            int width, int height, int format, String displayName) {
         return new VirtualCameraConfig.Builder()
                 .addStreamConfig(width, height, format)
-                .setDisplayNameStringRes(displayNameResId)
+                .setName(displayName)
                 .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback())
                 .build();
     }
@@ -156,10 +154,6 @@
                     @NonNull VirtualCameraStreamConfig streamConfig) {}
 
             @Override
-            public void onProcessCaptureRequest(
-                    int streamId, long frameId, @Nullable VirtualCameraMetadata metadata) {}
-
-            @Override
             public void onStreamClosed(int streamId) {}
         };
     }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
new file mode 100644
index 0000000..d9a38eb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.companion.virtual.camera;
+
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class VirtualCameraStreamConfigTest {
+
+    private static final int VGA_WIDTH = 640;
+    private static final int VGA_HEIGHT = 480;
+
+    private static final int QVGA_WIDTH = 320;
+    private static final int QVGA_HEIGHT = 240;
+
+    @Test
+    public void testEquals() {
+        VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
+                VGA_HEIGHT,
+                ImageFormat.YUV_420_888);
+        VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH,
+                QVGA_HEIGHT, ImageFormat.YUV_420_888);
+        VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
+                VGA_HEIGHT, PixelFormat.RGBA_8888);
+
+        new EqualsTester()
+                .addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig))
+                .addEqualityGroup(qvgaYuvStreamConfig, reparcel(qvgaYuvStreamConfig))
+                .addEqualityGroup(vgaRgbaStreamConfig, reparcel(vgaRgbaStreamConfig))
+                .testEquals();
+    }
+
+    private static VirtualCameraStreamConfig reparcel(VirtualCameraStreamConfig config) {
+        Parcel parcel = Parcel.obtain();
+        try {
+            config.writeToParcel(parcel, /* flags= */ 0);
+            parcel.setDataPosition(0);
+            return VirtualCameraStreamConfig.CREATOR.createFromParcel(parcel);
+        } finally {
+            parcel.recycle();
+        }
+    }
+
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java
index b2854ce..ef15f60 100644
--- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java
@@ -868,31 +868,6 @@
                 + "<compat-change id=\"1234\" name=\"MY_CHANGE1\" enableAfterTargetSdk=\"2\" />"
                 + "<compat-change id=\"1235\" name=\"MY_CHANGE2\" disabled=\"true\" />"
                 + "<compat-change id=\"1236\" name=\"MY_CHANGE3\" />"
-                + "</config>";
-
-        File dir = createTempDir();
-        writeToFile(dir, "platform_compat_config.xml", configXml);
-        CompatConfig compatConfig = new CompatConfig(mBuildClassifier, mContext);
-        compatConfig.forceNonDebuggableFinalForTest(false);
-
-        compatConfig.initConfigFromLib(dir);
-
-        assertThat(compatConfig.isChangeEnabled(1234L,
-            ApplicationInfoBuilder.create().withTargetSdk(1).build())).isFalse();
-        assertThat(compatConfig.isChangeEnabled(1234L,
-            ApplicationInfoBuilder.create().withTargetSdk(3).build())).isTrue();
-        assertThat(compatConfig.isChangeEnabled(1235L,
-            ApplicationInfoBuilder.create().withTargetSdk(5).build())).isFalse();
-        assertThat(compatConfig.isChangeEnabled(1236L,
-            ApplicationInfoBuilder.create().withTargetSdk(1).build())).isTrue();
-    }
-
-    @Test
-    public void testReadApexConfig() throws IOException {
-        String configXml = "<config>"
-                + "<compat-change id=\"1234\" name=\"MY_CHANGE1\" enableAfterTargetSdk=\"2\" />"
-                + "<compat-change id=\"1235\" name=\"MY_CHANGE2\" disabled=\"true\" />"
-                + "<compat-change id=\"1236\" name=\"MY_CHANGE3\" />"
                 + "<compat-change id=\"1237\" name=\"MY_CHANGE4\" enableSinceTargetSdk=\"31\" />"
                 + "</config>";
 
@@ -912,6 +887,12 @@
         assertThat(compatConfig.isChangeEnabled(1236L,
             ApplicationInfoBuilder.create().withTargetSdk(1).build())).isTrue();
         assertThat(compatConfig.isChangeEnabled(1237L,
+            ApplicationInfoBuilder.create().withTargetSdk(31).build())).isFalse();
+
+        // Force the platform sdk version to be same as enabled target sdk
+        when(mBuildClassifier.platformTargetSdk()).thenReturn(31);
+
+        assertThat(compatConfig.isChangeEnabled(1237L,
             ApplicationInfoBuilder.create().withTargetSdk(31).build())).isTrue();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
index 943a9c47..1dd64ff 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java
@@ -18,7 +18,6 @@
 import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.android.server.devicepolicy.DpmTestUtils.writeInputStreamToFile;
-import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -222,21 +221,21 @@
         prepareAdmin1AsPo(COPE_PROFILE_USER_ID, Build.VERSION_CODES.R);
 
         // Pretend some packages are suspended.
-        when(getServices().packageManagerInternal.isSuspendingAnyPackages(
-                PLATFORM_PACKAGE_NAME, USER_SYSTEM)).thenReturn(true);
+        when(getServices().packageManagerInternal.isAdminSuspendingAnyPackages(
+                USER_SYSTEM)).thenReturn(true);
 
         final DevicePolicyManagerServiceTestable dpms = bootDpmsUp();
 
         verify(getServices().packageManagerInternal, never())
-                .unsuspendForSuspendingPackage(PLATFORM_PACKAGE_NAME, USER_SYSTEM);
+                .unsuspendAdminSuspendedPackages(USER_SYSTEM);
 
         sendBroadcastWithUser(dpms, Intent.ACTION_USER_STARTED, USER_SYSTEM);
 
         // Verify that actual package suspension state is not modified after user start
         verify(getServices().packageManagerInternal, never())
-                .unsuspendForSuspendingPackage(PLATFORM_PACKAGE_NAME, USER_SYSTEM);
+                .unsuspendAdminSuspendedPackages(USER_SYSTEM);
         verify(getServices().ipackageManager, never()).setPackagesSuspendedAsUser(
-                any(), anyBoolean(), any(), any(), any(), anyInt(), any(), anyInt());
+                any(), anyBoolean(), any(), any(), any(), anyInt(), any(), anyInt(), anyInt());
 
         final DpmMockContext poContext = new DpmMockContext(getServices(), mRealTestContext);
         poContext.binder.callingUid = UserHandle.getUid(COPE_PROFILE_USER_ID, COPE_ADMIN1_APP_ID);
@@ -255,14 +254,14 @@
         prepareAdmin1AsPo(COPE_PROFILE_USER_ID, Build.VERSION_CODES.Q);
 
         // Pretend some packages are suspended.
-        when(getServices().packageManagerInternal.isSuspendingAnyPackages(
-                PLATFORM_PACKAGE_NAME, USER_SYSTEM)).thenReturn(true);
+        when(getServices().packageManagerInternal.isAdminSuspendingAnyPackages(
+                USER_SYSTEM)).thenReturn(true);
 
         final DevicePolicyManagerServiceTestable dpms = bootDpmsUp();
 
         // Verify that apps get unsuspended.
         verify(getServices().packageManagerInternal)
-                .unsuspendForSuspendingPackage(PLATFORM_PACKAGE_NAME, USER_SYSTEM);
+                .unsuspendAdminSuspendedPackages(USER_SYSTEM);
 
         final DpmMockContext poContext = new DpmMockContext(getServices(), mRealTestContext);
         poContext.binder.callingUid = UserHandle.getUid(COPE_PROFILE_USER_ID, COPE_ADMIN1_APP_ID);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index f4dac2c..2470403 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -63,7 +63,6 @@
 import static com.android.server.devicepolicy.DevicePolicyManagerService.ACTION_PROFILE_OFF_DEADLINE;
 import static com.android.server.devicepolicy.DevicePolicyManagerService.ACTION_TURN_PROFILE_ON_NOTIFICATION;
 import static com.android.server.devicepolicy.DpmMockContext.CALLER_USER_HANDLE;
-import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 import static com.android.server.testutils.TestUtils.assertExpectException;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -5080,7 +5079,7 @@
         verify(getServices().iwindowManager).refreshScreenCaptureDisabled();
         // Unsuspend personal apps
         verify(getServices().packageManagerInternal)
-                .unsuspendForSuspendingPackage(PLATFORM_PACKAGE_NAME, UserHandle.USER_SYSTEM);
+                .unsuspendAdminSuspendedPackages(UserHandle.USER_SYSTEM);
         verify(getServices().subscriptionManager).setSubscriptionUserHandle(0, null);
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
                 FLAG_ENABLE_WORK_PROFILE_TELEPHONY, "false", false);
@@ -7535,7 +7534,7 @@
                 .cancel(eq(SystemMessageProto.SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED));
         // Verify that the apps are NOT unsuspeded.
         verify(getServices().ipackageManager, never()).setPackagesSuspendedAsUser(
-                any(), eq(false), any(), any(), any(), anyInt(), any(), anyInt());
+                any(), eq(false), any(), any(), any(), anyInt(), any(), anyInt(), anyInt());
         // Verify that DPC is invoked to check policy compliance.
         verify(mContext.spiedContext).startActivityAsUser(
                 MockUtils.checkIntentAction(ACTION_CHECK_POLICY_COMPLIANCE),
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
index a63d01b..e7da26e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
@@ -456,6 +456,14 @@
     }
 
     @Test
+    public void avbEnabled_standby_avbDisabled() {
+        enableAbsoluteVolumeBehavior();
+        mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+        assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo(
+                AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+    }
+
+    @Test
     public void avbEnabled_cecVolumeDisabled_avbDisabled() {
         enableAbsoluteVolumeBehavior();
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 708ee35..99fa30c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -1706,6 +1706,15 @@
         verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
     }
 
+    @Test
+    public void onHotplugIn_invalidPortId_noAddressAllocation() {
+        mHdmiControlServiceSpy.onHotplug(-1, true);
+        mTestLooper.dispatchAll();
+
+        verify(mHdmiControlServiceSpy, times(0))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_HOTPLUG));
+    }
+
     protected static class MockPlaybackDevice extends HdmiCecLocalDevicePlayback {
 
         private boolean mCanGoToStandby;
diff --git a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
index ce15c6d..eb9cce0 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
@@ -67,10 +67,10 @@
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.R;
+import com.android.internal.pm.parsing.PackageParser2;
 import com.android.server.compat.PlatformCompat;
 import com.android.server.integrity.engine.RuleEvaluationEngine;
 import com.android.server.integrity.model.IntegrityCheckResult;
-import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.TestPackageParser2;
 import com.android.server.testutils.TestUtils;
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index b4281d63c..101ea6d 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -183,6 +183,13 @@
     }
 
     @Test
+    public void testIsUserInitialized_NonExistentUserReturnsFalse() {
+        int nonExistentUserId = UserHandle.USER_NULL;
+        assertThat(mUserManagerService.isUserInitialized(nonExistentUserId))
+                .isFalse();
+    }
+
+    @Test
     public void testSetUserRestrictionWithIncorrectID() throws Exception {
         int incorrectId = 1;
         while (mUserManagerService.userExists(incorrectId)) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java
index 1bfd43f..25eedf5 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java
@@ -37,6 +37,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.servicestests.R;
+import com.android.internal.pm.parsing.PackageParserException;
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.PackageManagerException;
 import com.android.server.pm.parsing.TestPackageParser2;
@@ -161,7 +162,8 @@
     }
 
     @Test
-    public void testParsePackageWithDmFileValid() throws IOException, PackageManagerException {
+    public void testParsePackageWithDmFileValid() throws IOException, PackageManagerException,
+            PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk");
         ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false);
@@ -178,7 +180,7 @@
 
     @Test
     public void testParsePackageSplitsWithDmFileValid()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageManagerException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
         createDexMetadataFile("install_split_base.apk");
@@ -201,7 +203,7 @@
 
     @Test
     public void testParsePackageSplitsNoBaseWithDmFileValid()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageManagerException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
         createDexMetadataFile("install_split_feature_a.apk");
@@ -219,7 +221,7 @@
     }
 
     @Test
-    public void testParsePackageWithDmFileInvalid() throws IOException {
+    public void testParsePackageWithDmFileInvalid() throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         File invalidDmFile = new File(mTmpDir, "install_split_base.dm");
         Files.createFile(invalidDmFile.toPath());
@@ -242,7 +244,7 @@
 
     @Test
     public void testParsePackageSplitsWithDmFileInvalid()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
         createDexMetadataFile("install_split_base.apk");
@@ -268,7 +270,7 @@
 
     @Test
     public void testParsePackageWithDmFileInvalidManifest()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk", /*validManifest=*/false);
 
@@ -283,7 +285,7 @@
 
     @Test
     public void testParsePackageWithDmFileEmptyManifest()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk", /*packageName=*/"doesn't matter",
                 /*versionCode=*/-12345L, /*emptyManifest=*/true, /*validManifest=*/true);
@@ -299,7 +301,7 @@
 
     @Test
     public void testParsePackageWithDmFileBadPackageName()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk", /*packageName=*/"bad package name",
                 DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, /*validManifest=*/true);
@@ -315,7 +317,7 @@
 
     @Test
     public void testParsePackageWithDmFileBadVersionCode()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk", DEX_METADATA_PACKAGE_NAME,
                 /*versionCode=*/12345L, /*emptyManifest=*/false, /*validManifest=*/true);
@@ -331,7 +333,7 @@
 
     @Test
     public void testParsePackageWithDmFileMissingPackageName()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk", /*packageName=*/null,
                 DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, /*validManifest=*/true);
@@ -347,7 +349,7 @@
 
     @Test
     public void testParsePackageWithDmFileMissingVersionCode()
-            throws IOException, PackageManagerException {
+            throws IOException, PackageParserException {
         copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
         createDexMetadataFile("install_split_base.apk", DEX_METADATA_PACKAGE_NAME,
                 /*versionCode=*/null, /*emptyManifest=*/false, /*validManifest=*/true);
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
index 9b4ca2a..6a088d9 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
@@ -36,7 +36,7 @@
 @Postsubmit
 class AndroidPackageParsingValidationTest {
     companion object {
-        private val parser2 = PackageParser2.forParsingFileWithDefaults()
+        private val parser2 = PackageParserUtils.forParsingFileWithDefaults()
         private val apks = ((PackageManagerService.SYSTEM_PARTITIONS)
                 .flatMap {
                     listOfNotNull(it.privAppFolder, it.appFolder, it.overlayFolder)
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/TestPackageParser2.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/TestPackageParser2.kt
index c44f583..e420e4b 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/TestPackageParser2.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/TestPackageParser2.kt
@@ -18,11 +18,12 @@
 
 import android.content.pm.ApplicationInfo
 import android.util.ArraySet
+import com.android.internal.pm.parsing.PackageParser2
 import java.io.File
 
 class TestPackageParser2(var cacheDir: File? = null) : PackageParser2(
         null /* separateProcesses */, null /* displayMetrics */,
-        cacheDir /* cacheDir */, object : PackageParser2.Callback() {
+    cacheDir?.let { PackageCacher(cacheDir) }, object : PackageParser2.Callback() {
     override fun isChangeEnabled(changeId: Long, appInfo: ApplicationInfo): Boolean {
         return true
     }
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
index 8487903..89056cc 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -64,17 +64,18 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.intThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.usage.AppStandbyInfo;
 import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
 import android.appwidget.AppWidgetManager;
 import android.content.Context;
@@ -99,7 +100,6 @@
 import android.view.Display;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -110,6 +110,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -125,6 +126,7 @@
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 
 /**
@@ -175,6 +177,9 @@
     private static final int ASSERT_RETRY_ATTEMPTS = 20;
     private static final int ASSERT_RETRY_DELAY_MILLISECONDS = 500;
 
+    @DurationMillisLong
+    private static final long FLUSH_TIMEOUT_MILLISECONDS = 5_000;
+
     /** Mock variable used in {@link MyInjector#isPackageInstalled(String, int, int)} */
     private static boolean isPackageInstalled = true;
 
@@ -563,6 +568,7 @@
         mInjector = new MyInjector(myContext, Looper.getMainLooper());
         mController = setupController();
         setupInitialUsageHistory();
+        flushHandler(mController);
     }
 
     @After
@@ -571,12 +577,9 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testBoundWidgetPackageExempt() throws Exception {
         assumeTrue(mInjector.getContext().getSystemService(AppWidgetManager.class) != null);
-        assertEquals(STANDBY_BUCKET_ACTIVE,
-                mController.getAppStandbyBucket(PACKAGE_EXEMPTED_1, USER_ID,
-                        mInjector.mElapsedRealtime, false));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_EXEMPTED_1);
     }
 
     @Test
@@ -654,7 +657,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testIsAppIdle_Charging() throws Exception {
         TestParoleListener paroleListener = new TestParoleListener();
         mController.addListener(paroleListener);
@@ -662,7 +664,7 @@
         setChargingState(mController, false);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
         assertFalse(mController.isInParole());
@@ -671,7 +673,7 @@
         setChargingState(mController, true);
         paroleListener.awaitOnLatch(2000);
         assertTrue(paroleListener.getParoleState());
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
         assertTrue(mController.isInParole());
@@ -680,14 +682,13 @@
         setChargingState(mController, false);
         paroleListener.awaitOnLatch(2000);
         assertFalse(paroleListener.getParoleState());
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
         assertFalse(mController.isInParole());
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testIsAppIdle_Enabled() throws Exception {
         setChargingState(mController, false);
         TestParoleListener paroleListener = new TestParoleListener();
@@ -696,7 +697,7 @@
         setAppIdleEnabled(mController, true);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
 
@@ -711,7 +712,7 @@
         setAppIdleEnabled(mController, true);
         paroleListener.awaitOnLatch(2000);
         assertFalse(paroleListener.getParoleState());
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false));
     }
@@ -723,6 +724,7 @@
     private void assertTimeout(AppStandbyController controller, long elapsedTime, int bucket,
             int userId) {
         mInjector.mElapsedRealtime = elapsedTime;
+        flushHandler(controller);
         controller.checkIdleStates(userId);
         assertEquals(bucket,
                 controller.getAppStandbyBucket(PACKAGE_1, userId, mInjector.mElapsedRealtime,
@@ -744,50 +746,85 @@
     }
 
     private int getStandbyBucket(int userId, AppStandbyController controller, String packageName) {
+        flushHandler(controller);
         return controller.getAppStandbyBucket(packageName, userId, mInjector.mElapsedRealtime,
                 true);
     }
 
+    private List<AppStandbyInfo> getStandbyBuckets(int userId) {
+        flushHandler(mController);
+        return mController.getAppStandbyBuckets(userId);
+    }
+
     private int getStandbyBucketReason(String packageName) {
+        flushHandler(mController);
         return mController.getAppStandbyBucketReason(packageName, USER_ID,
                 mInjector.mElapsedRealtime);
     }
 
-    private void assertBucket(int bucket) throws InterruptedException {
-        assertBucket(bucket, PACKAGE_1);
+    private void waitAndAssertBucket(int bucket, String pkg) {
+        waitAndAssertBucket(mController, bucket, pkg);
     }
 
-    private void assertBucket(int bucket, String pkg) throws InterruptedException {
-        int retries = 0;
-        do {
-            if (bucket == getStandbyBucket(mController, pkg)) {
-                // Success
-                return;
-            }
-            Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS);
-            retries++;
-        } while(retries < ASSERT_RETRY_ATTEMPTS);
-        // try one last time
-        assertEquals(bucket, getStandbyBucket(mController, pkg));
+    private void waitAndAssertBucket(AppStandbyController controller, int bucket, String pkg) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(pkg);
+        sb.append(" was not in the ");
+        sb.append(UsageStatsManager.standbyBucketToString(bucket));
+        sb.append(" (");
+        sb.append(bucket);
+        sb.append(") bucket.");
+        waitAndAssertBucket(sb.toString(), controller, bucket, pkg);
     }
 
-    private void assertNotBucket(int bucket) throws InterruptedException {
-        final String pkg = PACKAGE_1;
+    private void waitAndAssertBucket(String msg, int bucket, String pkg) {
+        waitAndAssertBucket(msg, mController, bucket, pkg);
+    }
+
+    private void waitAndAssertBucket(String msg, AppStandbyController controller, int bucket,
+            String pkg) {
+        waitAndAssertBucket(msg, controller, bucket, USER_ID, pkg);
+    }
+
+    private void waitAndAssertBucket(String msg, AppStandbyController controller, int bucket,
+            int userId,
+            String pkg) {
+        waitUntil(() -> bucket == getStandbyBucket(userId, controller, pkg));
+        assertEquals(msg, bucket, getStandbyBucket(userId, controller, pkg));
+    }
+
+    private void waitAndAssertNotBucket(int bucket, String pkg) {
+        waitAndAssertNotBucket(mController, bucket, pkg);
+    }
+
+    private void waitAndAssertNotBucket(AppStandbyController controller, int bucket, String pkg) {
+        waitUntil(() -> bucket != getStandbyBucket(controller, pkg));
+        assertNotEquals(bucket, getStandbyBucket(controller, pkg));
+    }
+
+    private void waitAndAssertLastNoteEvent(int event) {
+        waitUntil(() -> {
+            flushHandler(mController);
+            return event == mInjector.mLastNoteEvent;
+        });
+        assertEquals(event, mInjector.mLastNoteEvent);
+    }
+
+    // Waits until condition is true or times out.
+    private void waitUntil(BooleanSupplier resultSupplier) {
         int retries = 0;
         do {
-            if (bucket != getStandbyBucket(mController, pkg)) {
-                // Success
-                return;
+            if (resultSupplier.getAsBoolean()) return;
+            try {
+                Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS);
+            } catch (InterruptedException ie) {
+                // Do nothing
             }
-            Thread.sleep(ASSERT_RETRY_DELAY_MILLISECONDS);
             retries++;
-        } while(retries < ASSERT_RETRY_ATTEMPTS);
-        // try one last time
-        assertNotEquals(bucket, getStandbyBucket(mController, pkg));
+        } while (retries < ASSERT_RETRY_ATTEMPTS);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testBuckets() throws Exception {
         assertTimeout(mController, 0, STANDBY_BUCKET_NEVER);
 
@@ -820,14 +857,13 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSetAppStandbyBucket() throws Exception {
         // For a known package, standby bucket should be set properly
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_TIMEOUT);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // For an unknown package, standby bucket should not be set, hence NEVER is returned
         // Ensure the unknown package is not already in history by removing it
@@ -836,21 +872,20 @@
         mController.setAppStandbyBucket(PACKAGE_UNKNOWN, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_TIMEOUT);
         isPackageInstalled = true; // Reset mocked variable for other tests
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_UNKNOWN));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_UNKNOWN);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testAppStandbyBucketOnInstallAndUninstall() throws Exception {
         // On package install, standby bucket should be ACTIVE
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_UNKNOWN);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_UNKNOWN));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_UNKNOWN);
 
         // On uninstall, package should not exist in history and should return a NEVER bucket
         mController.clearAppIdleForPackage(PACKAGE_UNKNOWN, USER_ID);
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_UNKNOWN));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_UNKNOWN);
         // Ensure uninstalled app is not in history
-        List<AppStandbyInfo> buckets = mController.getAppStandbyBuckets(USER_ID);
+        List<AppStandbyInfo> buckets = getStandbyBuckets(USER_ID);
         for(AppStandbyInfo bucket : buckets) {
             if (bucket.mPackageName.equals(PACKAGE_UNKNOWN)) {
                 fail("packageName found in app idle history after uninstall.");
@@ -859,7 +894,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testScreenTimeAndBuckets() throws Exception {
         mInjector.setDisplayOn(false);
 
@@ -876,22 +910,21 @@
         // RARE bucket, should fail because the screen wasn't ON.
         mInjector.mElapsedRealtime = RARE_THRESHOLD + 1;
         mController.checkIdleStates(USER_ID);
-        assertNotEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertNotBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mInjector.setDisplayOn(true);
         assertTimeout(mController, RARE_THRESHOLD + 2 * HOUR_MS + 1, STANDBY_BUCKET_RARE);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testForcedIdle() throws Exception {
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
 
         mController.forceIdleState(PACKAGE_1, USER_ID, false);
-        assertEquals(STANDBY_BUCKET_ACTIVE, mController.getAppStandbyBucket(PACKAGE_1, USER_ID, 0,
-                true));
+
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
     }
 
@@ -901,15 +934,15 @@
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime = 1;
         rearmQuotaBumpLatch(PACKAGE_1, USER_ID);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
         assertFalse(mQuotaBumpLatch.await(1, TimeUnit.SECONDS));
     }
 
@@ -917,9 +950,10 @@
     public void testNotificationEvent_bucketPromotion_changePromotedBucket() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+        mInjector.mElapsedRealtime += RARE_THRESHOLD + 1;
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // TODO: Avoid hardcoding these string constants.
         mInjector.mSettingsBuilder.setInt("notification_seen_promoted_bucket",
@@ -928,11 +962,10 @@
                 mInjector.getDeviceConfigProperties());
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testNotificationEvent_quotaBump() throws Exception {
         mInjector.mSettingsBuilder
                 .setBoolean("trigger_quota_bump_on_notification_seen", true);
@@ -942,7 +975,7 @@
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime = RARE_THRESHOLD * 2;
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
 
@@ -951,83 +984,80 @@
 
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
         assertTrue(mQuotaBumpLatch.await(1, TimeUnit.SECONDS));
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSlicePinnedEvent() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime = 1;
         reportEvent(mController, SLICE_PINNED, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, SLICE_PINNED, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSlicePinnedPrivEvent() throws Exception {
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, SLICE_PINNED_PRIV, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionTimedOut() throws Exception {
         // Set it to timeout or usage, so that prediction can override it
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Fast forward 12 hours
         mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD;
         mController.checkIdleStates(USER_ID);
         // Should still be in predicted bucket, since prediction timeout is 1 day since prediction
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         // Fast forward two more hours
         mInjector.mElapsedRealtime += 2 * HOUR_MS;
         mController.checkIdleStates(USER_ID);
         // Should have now applied prediction timeout
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // Fast forward RARE bucket
         mInjector.mElapsedRealtime += RARE_THRESHOLD;
         mController.checkIdleStates(USER_ID);
         // Should continue to apply prediction timeout
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
+    @Ignore("b/317086276")
     public void testOverrides() throws Exception {
         // Can force to NEVER
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1);
 
         // Prediction can't override FORCED reasons
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         // Prediction can't override NEVER
         mInjector.mElapsedRealtime = 2 * HOUR_MS;
@@ -1035,115 +1065,114 @@
                 REASON_MAIN_DEFAULT);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_NEVER, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1);
 
         // Prediction can't set to NEVER
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_USAGE);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Prediction can't remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Force from user can remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // Force from system can remove from RESTRICTED if it was put it in due to system
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_PREDICTED);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Non-user usage can't remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_SYSTEM_INTERACTION);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_SYNC_ADAPTER);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Explicit user usage can remove from RESTRICTED
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION);
-        assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_USAGE | REASON_SUB_USAGE_MOVE_TO_FOREGROUND);
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testTimeout() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime = mController.mPredictionTimeoutMillis - 100;
         mController.checkIdleStates(USER_ID);
         // Use recent prediction
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         // Way past prediction timeout, use system thresholds
         mInjector.mElapsedRealtime = RARE_THRESHOLD;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     /** Test that timeouts still work properly even if invalid configuration values are set. */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testTimeout_InvalidThresholds() throws Exception {
         mInjector.mSettingsBuilder
                 .setLong("screen_threshold_active", -1)
@@ -1161,19 +1190,19 @@
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = HOUR_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2 * HOUR_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 4 * HOUR_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
@@ -1181,74 +1210,72 @@
      * timeout has passed.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
+    @Ignore("b/317086276")
     public void testTimeoutBeforeRestricted() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         // Bucket shouldn't change
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime += DAY_MS;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Way past all timeouts. Make sure timeout processing doesn't raise bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
      * Test that an app is put into the RESTRICTED bucket after enough time has passed.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedDelay() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime += mInjector.getAutoRestrictedBucketDelayMs() - 5000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         // Bucket shouldn't change
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime += 6000;
 
         Thread.sleep(6000);
         // Enough time has passed. The app should automatically be put into the RESTRICTED bucket.
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
      * Test that an app is put into the RESTRICTED bucket after enough time has passed.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedDelay_DelayChange() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mAutoRestrictedBucketDelayMs = 2 * HOUR_MS;
         mInjector.mElapsedRealtime += 2 * HOUR_MS - 5000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM);
         // Bucket shouldn't change
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // bucketing works after timeout
         mInjector.mElapsedRealtime += 6000;
 
         Thread.sleep(6000);
         // Enough time has passed. The app should automatically be put into the RESTRICTED bucket.
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
@@ -1256,36 +1283,35 @@
      * a low bucket after the RESTRICTED timeout.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedTimeoutOverridesRestoredLowBucketPrediction() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Predict to RARE Not long enough to time out into RESTRICTED.
         mInjector.mElapsedRealtime += RARE_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         // Add a short timeout event
         mInjector.mElapsedRealtime += 1000;
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Long enough that it could have timed out into RESTRICTED. Instead of reverting to
         // predicted RARE, should go into RESTRICTED
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Ensure that prediction can still raise it out despite this override.
         mInjector.mElapsedRealtime += 1;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     /**
@@ -1293,7 +1319,6 @@
      * a low bucket after the RESTRICTED timeout.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictedTimeoutOverridesPredictionLowBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
@@ -1301,7 +1326,7 @@
         mInjector.mElapsedRealtime += RARE_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         mInjector.mElapsedRealtime += 1;
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
@@ -1310,10 +1335,10 @@
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     /**
@@ -1321,261 +1346,250 @@
      * interaction.
      */
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemInteractionOverridesRestrictedTimeout() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Long enough that it could have timed out into RESTRICTED.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Report system interaction.
         mInjector.mElapsedRealtime += 1000;
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
         // Ensure that it's raised out of RESTRICTED for the system interaction elevation duration.
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Elevation duration over. Should fall back down.
         mInjector.mElapsedRealtime += 10 * MINUTE_MS;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionRaiseFromRestrictedTimeout_highBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
         // Way past all timeouts. App times out into RESTRICTED bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Since the app timed out into RESTRICTED, prediction should be able to remove from the
         // bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionRaiseFromRestrictedTimeout_lowBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
 
         // Way past all timeouts. App times out into RESTRICTED bucket.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Prediction into a low bucket means no expectation of the app being used, so we shouldn't
         // elevate the app from RESTRICTED.
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testCascadingTimeouts() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         reportEvent(mController, NOTIFICATION_SEEN, 1000, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2000 + mController.mStrongUsageTimeoutMillis;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_WORKING_SET);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mInjector.mElapsedRealtime = 2000 + mController.mNotificationSeenTimeoutMillis;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testOverlappingTimeouts() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         reportEvent(mController, NOTIFICATION_SEEN, 1000, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Overlapping USER_INTERACTION before previous one times out
         reportEvent(mController, USER_INTERACTION, mController.mStrongUsageTimeoutMillis - 1000,
                 PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Still in ACTIVE after first USER_INTERACTION times out
         mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis + 1000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Both timed out, so NOTIFICATION_SEEN timeout should be effective
         mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis * 2 + 2000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_WORKING_SET);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         mInjector.mElapsedRealtime = mController.mNotificationSeenTimeoutMillis + 2000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemInteractionTimeout() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         // Fast forward to RARE
         mInjector.mElapsedRealtime = RARE_THRESHOLD + 100;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         // Trigger a SYSTEM_INTERACTION and verify bucket
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify it's still in ACTIVE close to end of timeout
         mInjector.mElapsedRealtime += mController.mSystemInteractionTimeoutMillis - 100;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify bucket moves to RARE after timeout
         mInjector.mElapsedRealtime += 200;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testInitialForegroundServiceTimeout() throws Exception {
         mInjector.mElapsedRealtime = 1 * RARE_THRESHOLD + 100;
         // Make sure app is in NEVER bucket
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
                 REASON_MAIN_FORCED_BY_USER);
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_NEVER);
+        waitAndAssertBucket(STANDBY_BUCKET_NEVER, PACKAGE_1);
 
         mInjector.mElapsedRealtime += 100;
 
         // Trigger a FOREGROUND_SERVICE_START and verify bucket
         reportEvent(mController, FOREGROUND_SERVICE_START, mInjector.mElapsedRealtime, PACKAGE_1);
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify it's still in ACTIVE close to end of timeout
         mInjector.mElapsedRealtime += mController.mInitialForegroundServiceStartTimeoutMillis - 100;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify bucket moves to RARE after timeout
         mInjector.mElapsedRealtime += 200;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
 
         // Trigger a FOREGROUND_SERVICE_START again
         reportEvent(mController, FOREGROUND_SERVICE_START, mInjector.mElapsedRealtime, PACKAGE_1);
         mController.checkIdleStates(USER_ID);
         // Bucket should not be immediately elevated on subsequent service starts
-        assertBucket(STANDBY_BUCKET_RARE);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionNotOverridden() throws Exception {
         mInjector.mPropertiesChangedListener
                 .onPropertiesChanged(mInjector.getDeviceConfigProperties());
 
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mInjector.mElapsedRealtime = WORKING_SET_THRESHOLD - 1000;
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Falls back to WORKING_SET
         mInjector.mElapsedRealtime += 5000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_WORKING_SET);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // Predict to ACTIVE
         mInjector.mElapsedRealtime += 1000;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // CheckIdleStates should not change the prediction
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testPredictionStrikesBack() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Predict to FREQUENT
         mInjector.mElapsedRealtime = RARE_THRESHOLD;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_PREDICTED);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
 
         // Add a short timeout event
         mInjector.mElapsedRealtime += 1000;
         reportEvent(mController, SYSTEM_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += 1000;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_ACTIVE);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         // Verify it reverted to predicted
         mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD / 2;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_FREQUENT);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemForcedFlags_NotAddedForUserForce() throws Exception {
         final int expectedReason = REASON_MAIN_FORCED_BY_USER;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(expectedReason, getStandbyBucketReason(PACKAGE_1));
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(expectedReason, getStandbyBucketReason(PACKAGE_1));
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemForcedFlags_AddedForSystemForce() throws Exception {
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_DEFAULT);
@@ -1584,13 +1598,13 @@
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE,
                 getStandbyBucketReason(PACKAGE_1));
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         // Flags should be combined
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                 | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE
@@ -1598,7 +1612,6 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testSystemForcedFlags_SystemForceChangesBuckets() throws Exception {
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_DEFAULT);
@@ -1607,14 +1620,14 @@
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE,
                 getStandbyBucketReason(PACKAGE_1));
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE);
-        assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_1);
         // Flags should be combined
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE
@@ -1623,20 +1636,19 @@
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
-        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
         // Flags should be combined
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY,
                 getStandbyBucketReason(PACKAGE_1));
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED);
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         // Flags should not be combined since the bucket changed.
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED,
                 getStandbyBucketReason(PACKAGE_1));
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testRestrictApp_MainReason() throws Exception {
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_DEFAULT);
@@ -1644,11 +1656,11 @@
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_MAIN_PREDICTED, 0);
         // Call should be ignored.
-        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         mController.restrictApp(PACKAGE_1, USER_ID, REASON_MAIN_FORCED_BY_USER, 0);
         // Call should go through
-        assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
@@ -1724,15 +1736,15 @@
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testUserInteraction_CrossProfile() throws Exception {
         mInjector.mRunningUsers = new int[] {USER_ID, USER_ID2, USER_ID3};
         mInjector.mCrossProfileTargets = Arrays.asList(USER_HANDLE_USER2);
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals("Cross profile connected package bucket should be elevated on usage",
-                STANDBY_BUCKET_ACTIVE, getStandbyBucket(USER_ID2, mController, PACKAGE_1));
-        assertEquals("Not Cross profile connected package bucket should not be elevated on usage",
-                STANDBY_BUCKET_NEVER, getStandbyBucket(USER_ID3, mController, PACKAGE_1));
+        waitAndAssertBucket("Cross profile connected package bucket should be elevated on usage",
+                mController, STANDBY_BUCKET_ACTIVE, USER_ID2, PACKAGE_1);
+        waitAndAssertBucket(
+                "Not Cross profile connected package bucket should not be elevated on usage",
+                mController, STANDBY_BUCKET_NEVER, USER_ID3, PACKAGE_1);
 
         assertTimeout(mController, WORKING_SET_THRESHOLD - 1, STANDBY_BUCKET_ACTIVE, USER_ID);
         assertTimeout(mController, WORKING_SET_THRESHOLD - 1, STANDBY_BUCKET_ACTIVE, USER_ID2);
@@ -1742,51 +1754,50 @@
 
         mInjector.mCrossProfileTargets = Collections.emptyList();
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
-        assertEquals("No longer cross profile connected package bucket should not be "
-                        + "elevated on usage",
-                STANDBY_BUCKET_WORKING_SET, getStandbyBucket(USER_ID2, mController, PACKAGE_1));
+        waitAndAssertBucket("No longer cross profile connected package bucket should not be "
+                        + "elevated on usage", mController, STANDBY_BUCKET_WORKING_SET, USER_ID2,
+                PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testUnexemptedSyncScheduled() throws Exception {
         rearmLatch(PACKAGE_1);
         mController.addListener(mListener);
-        assertEquals("Test package did not start in the Never bucket", STANDBY_BUCKET_NEVER,
-                getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Test package did not start in the Never bucket", STANDBY_BUCKET_NEVER,
+                PACKAGE_1);
 
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, false);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Unexempted sync scheduled should bring the package out of the Never bucket",
-                STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket(
+                "Unexempted sync scheduled should bring the package out of the Never bucket",
+                STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
 
         rearmLatch(PACKAGE_1);
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, false);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Unexempted sync scheduled should not elevate a non Never bucket",
-                STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Unexempted sync scheduled should not elevate a non Never bucket",
+                STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
-    @FlakyTest(bugId = 185169504)
     public void testExemptedSyncScheduled() throws Exception {
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
         mInjector.mDeviceIdleMode = true;
         rearmLatch(PACKAGE_1);
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, true);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Exempted sync scheduled in doze should set bucket to working set",
-                STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Exempted sync scheduled in doze should set bucket to working set",
+                STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
         mInjector.mDeviceIdleMode = false;
         rearmLatch(PACKAGE_1);
         mController.postReportSyncScheduled(PACKAGE_1, USER_ID, true);
         mStateChangedLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals("Exempted sync scheduled while not in doze should set bucket to active",
-                STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Exempted sync scheduled while not in doze should set bucket to active",
+                STANDBY_BUCKET_ACTIVE, PACKAGE_1);
     }
 
     @Test
@@ -1796,14 +1807,14 @@
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD * 4;
         mController.checkIdleStates(USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates shouldn't change bucket if the app was forced by the system for a non-buggy
         // reason.
@@ -1814,11 +1825,11 @@
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates should change bucket if the app was forced by the system for a buggy reason.
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
@@ -1827,11 +1838,11 @@
                 REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertNotEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertNotBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates shouldn't change bucket if the app was forced by the system for more than just
         // a buggy reason.
@@ -1842,13 +1853,13 @@
                         | REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         assertEquals(REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE,
                 getStandbyBucketReason(PACKAGE_1));
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
 
         // Updates shouldn't change bucket if the app was forced by the user.
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
@@ -1857,11 +1868,11 @@
                 REASON_MAIN_FORCED_BY_USER);
 
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp(PACKAGE_1, USER_ID2);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
         mController.maybeUnrestrictBuggyApp("com.random.package", USER_ID);
-        assertBucket(STANDBY_BUCKET_RESTRICTED);
+        waitAndAssertBucket(STANDBY_BUCKET_RESTRICTED, PACKAGE_1);
     }
 
     @Test
@@ -1876,37 +1887,37 @@
 
         mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADFULL, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL);
 
         // Make sure headless system apps don't get lowered.
         mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADLESS, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS);
 
         // Package 1 doesn't have activities and is headless, but is not a system app, so it can
         // be lowered.
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
     public void testWellbeingAppElevated() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_WELLBEING);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING);
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
 
         // Make sure the default wellbeing app does not get lowered below WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_WELLBEING, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING);
 
         // A non default wellbeing app should be able to fall lower than WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
     }
 
     @Test
@@ -1914,22 +1925,22 @@
         mInjector.mClockApps.add(Pair.create(PACKAGE_1, UID_1));
 
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
 
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_2);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_2);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_2);
 
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
 
         // Make sure a clock app does not get lowered below WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
+        waitAndAssertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_1);
 
         // A non clock app should be able to fall lower than WORKING_SET.
         mController.setAppStandbyBucket(PACKAGE_2, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_RARE, PACKAGE_2);
+        waitAndAssertBucket(STANDBY_BUCKET_RARE, PACKAGE_2);
     }
 
     @Test
@@ -2067,13 +2078,13 @@
     public void testBackgroundLocationBucket() throws Exception {
         reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime,
                 PACKAGE_BACKGROUND_LOCATION);
-        assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_BACKGROUND_LOCATION);
+        waitAndAssertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_BACKGROUND_LOCATION);
 
         mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
         // Make sure PACKAGE_BACKGROUND_LOCATION does not get lowered than STANDBY_BUCKET_FREQUENT.
         mController.setAppStandbyBucket(PACKAGE_BACKGROUND_LOCATION, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_TIMEOUT);
-        assertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_BACKGROUND_LOCATION);
+        waitAndAssertBucket(STANDBY_BUCKET_FREQUENT, PACKAGE_BACKGROUND_LOCATION);
     }
 
     @Test
@@ -2083,41 +2094,41 @@
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE);
 
         // Since we're staying on the PACKAGE_ACTIVE side, noteEvent shouldn't be called.
         // Reset the last event to confirm the method isn't called.
         mInjector.mLastNoteEvent = BatteryStats.HistoryItem.EVENT_NONE;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_NONE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_NONE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE);
 
         // Since we're staying on the PACKAGE_ACTIVE side, noteEvent shouldn't be called.
         // Reset the last event to confirm the method isn't called.
         mInjector.mLastNoteEvent = BatteryStats.HistoryItem.EVENT_NONE;
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_NONE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_NONE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE);
 
         mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_EXEMPTED,
                 REASON_MAIN_FORCED_BY_USER);
-        assertEquals(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE, mInjector.mLastNoteEvent);
+        waitAndAssertLastNoteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE);
     }
 
     private String getAdminAppsStr(int userId) {
@@ -2187,8 +2198,7 @@
         rearmLatch(pkg);
         mController.setAppStandbyBucket(pkg, user, bucket, reason);
         mStateChangedLatch.await(1, TimeUnit.SECONDS);
-        assertEquals("Failed to set package bucket", bucket,
-                getStandbyBucket(mController, PACKAGE_1));
+        waitAndAssertBucket("Failed to set package bucket", bucket, PACKAGE_1);
     }
 
     private void rearmLatch(String pkgName) {
@@ -2205,4 +2215,12 @@
         mLatchUserId = userId;
         mQuotaBumpLatch = new CountDownLatch(1);
     }
+
+    private void flushHandler(AppStandbyController controller) {
+        assertTrue("Failed to flush handler!", controller.flushHandler(FLUSH_TIMEOUT_MILLISECONDS));
+        // Some AppStandbyController handler messages queue another handler message. Flush again
+        // to catch those as well.
+        assertTrue("Failed to flush handler (the second time)!",
+                controller.flushHandler(FLUSH_TIMEOUT_MILLISECONDS));
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 260ee396..30843d2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -245,8 +245,8 @@
     }
 
     @Test
-    @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}",
-            "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}"})
+    @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}",
+            "{origin: ORIGIN_INIT_USER}"})
     public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(ChangeOrigin origin) {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
 
@@ -263,7 +263,7 @@
 
     @Test
     @TestParameters({"{origin: ORIGIN_APP}", "{origin: ORIGIN_RESTORE_BACKUP}",
-            "{origin: ORIGIN_UNKNOWN}"})
+            "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}", "{origin: ORIGIN_UNKNOWN}"})
     public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin(ChangeOrigin origin) {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 2868b7e..ae36839 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -68,10 +68,7 @@
 import android.os.Parcel;
 import android.os.RemoteException;
 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.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.INotificationListener;
 import android.service.notification.NotificationListenerFilter;
 import android.service.notification.NotificationListenerService;
@@ -111,7 +108,7 @@
 public class NotificationListenersTest extends UiServiceTestCase {
 
     @Rule
-    public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Mock
     private PackageManager mPm;
@@ -696,8 +693,8 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testListenerTrusted_withPermission() throws RemoteException {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         when(mNm.mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, mUid1))
                 .thenReturn(PERMISSION_GRANTED);
         ManagedServices.ManagedServiceInfo info = getMockServiceInfo();
@@ -706,8 +703,8 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testListenerTrusted_withSystemSignature() {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         when(mNm.mPackageManagerInternal.isPlatformSigned(mCn1.getPackageName())).thenReturn(true);
         ManagedServices.ManagedServiceInfo info = getMockServiceInfo();
         mListeners.onServiceAdded(info);
@@ -715,8 +712,8 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testListenerTrusted_withCdmAssociation() throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         mNm.mCompanionManager = mock(ICompanionDeviceManager.class);
         AssociationInfo assocInfo = mock(AssociationInfo.class);
         when(assocInfo.isRevoked()).thenReturn(false);
@@ -731,16 +728,16 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testListenerTrusted_ifFlagDisabled() {
+        mSetFlagsRule.disableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         ManagedServices.ManagedServiceInfo info = getMockServiceInfo();
         mListeners.onServiceAdded(info);
         assertTrue(mListeners.isUidTrusted(mUid1));
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testRedaction_whenPosted() {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>();
         infos.add(getMockServiceInfo());
         doReturn(infos).when(mListeners).getServices();
@@ -762,13 +759,11 @@
         mListeners.notifyPostedLocked(r, old);
         verify(mListeners, atLeast(1)).redactStatusBarNotification(eq(sbn));
         verify(mListeners, never()).redactStatusBarNotification(eq(oldSbn));
-
-
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testRedaction_whenPosted_oldRemoved() {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>();
         infos.add(getMockServiceInfo());
         doReturn(infos).when(mListeners).getServices();
@@ -795,8 +790,8 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testRedaction_whenRemoved() {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         doReturn(mock(StatusBarNotification.class))
                 .when(mListeners).redactStatusBarNotification(any());
         ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>();
@@ -816,8 +811,8 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testRedaction_noneIfFlagDisabled() {
+        mSetFlagsRule.disableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         ArrayList<ManagedServices.ManagedServiceInfo> infos = new ArrayList<>();
         infos.add(getMockServiceInfo());
         doReturn(infos).when(mListeners).getServices();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 48c00a8..3ab7496 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -75,10 +75,16 @@
 import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS;
 import static android.service.notification.Adjustment.KEY_IMPORTANCE;
+import static android.service.notification.Adjustment.KEY_TEXT_REPLIES;
 import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
 import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
+import static android.service.notification.Condition.SOURCE_CONTEXT;
+import static android.service.notification.Condition.SOURCE_USER_ACTION;
+import static android.service.notification.Condition.STATE_TRUE;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -212,9 +218,6 @@
 import android.os.WorkSource;
 import android.permission.PermissionManager;
 import android.platform.test.annotations.EnableFlags;
-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.platform.test.rule.DeniedDevices;
 import android.platform.test.rule.DeviceProduct;
@@ -223,6 +226,7 @@
 import android.provider.MediaStore;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
+import android.service.notification.Condition;
 import android.service.notification.ConversationChannelWrapper;
 import android.service.notification.DeviceEffectsApplier;
 import android.service.notification.INotificationListener;
@@ -292,6 +296,7 @@
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.ClassRule;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -341,6 +346,11 @@
     private static final String SCHEME_TIMEOUT = "timeout";
     private static final String REDACTED_TEXT = "redacted text";
 
+    private static final AutomaticZenRule SOME_ZEN_RULE =
+            new AutomaticZenRule.Builder("rule", Uri.parse("uri"))
+                    .setOwner(new ComponentName("pkg", "cls"))
+                    .build();
+
     private final int mUid = Binder.getCallingUid();
     private final @UserIdInt int mUserId = UserHandle.getUserId(mUid);
 
@@ -350,9 +360,6 @@
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
 
-    @Rule
-    public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
     private TestableNotificationManagerService mService;
     private INotificationManager mBinderService;
     private NotificationManagerInternal mInternalService;
@@ -474,7 +481,7 @@
     TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
 
     TestableFlagResolver mTestFlagResolver = new TestableFlagResolver();
-    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
     private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
             1 << 30);
     @Mock
@@ -640,6 +647,8 @@
                 });
 
         // TODO (b/291907312): remove feature flag
+        // NOTE: Prefer using the @EnableFlag annotation where possible. Do not add any android.app
+        //  flags here.
         mSetFlagsRule.disableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER,
                 Flags.FLAG_POLITE_NOTIFICATIONS);
         initNMS();
@@ -6316,15 +6325,22 @@
         verify(visitor, times(1)).accept(eq(personIcon3.getUri()));
     }
 
+    private PendingIntent getPendingIntentWithUri(Uri uri) {
+        return PendingIntent.getActivity(mContext, 0,
+                new Intent("action", uri),
+                PendingIntent.FLAG_IMMUTABLE);
+    }
+
     @Test
-    public void testVisitUris_callStyle() {
+    @EnableFlags({android.app.Flags.FLAG_VISIT_RISKY_URIS})
+    public void testVisitUris_callStyle_ongoingCall() {
         Icon personIcon = Icon.createWithContentUri("content://media/person");
         Icon verificationIcon = Icon.createWithContentUri("content://media/verification");
         Person callingPerson = new Person.Builder().setName("Someone")
                 .setIcon(personIcon)
                 .build();
-        PendingIntent hangUpIntent = PendingIntent.getActivity(mContext, 0, new Intent(),
-                PendingIntent.FLAG_IMMUTABLE);
+        Uri hangUpUri = Uri.parse("content://intent/hangup");
+        PendingIntent hangUpIntent = getPendingIntentWithUri(hangUpUri);
         Notification n = new Notification.Builder(mContext, "a")
                 .setStyle(Notification.CallStyle.forOngoingCall(callingPerson, hangUpIntent)
                         .setVerificationIcon(verificationIcon))
@@ -6337,6 +6353,36 @@
 
         verify(visitor, times(1)).accept(eq(personIcon.getUri()));
         verify(visitor, times(1)).accept(eq(verificationIcon.getUri()));
+        verify(visitor, times(1)).accept(eq(hangUpUri));
+    }
+
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_VISIT_RISKY_URIS})
+    public void testVisitUris_callStyle_incomingCall() {
+        Icon personIcon = Icon.createWithContentUri("content://media/person");
+        Icon verificationIcon = Icon.createWithContentUri("content://media/verification");
+        Person callingPerson = new Person.Builder().setName("Someone")
+                .setIcon(personIcon)
+                .build();
+        Uri answerUri = Uri.parse("content://intent/answer");
+        PendingIntent answerIntent = getPendingIntentWithUri(answerUri);
+        Uri declineUri = Uri.parse("content://intent/decline");
+        PendingIntent declineIntent = getPendingIntentWithUri(declineUri);
+        Notification n = new Notification.Builder(mContext, "a")
+                .setStyle(Notification.CallStyle.forIncomingCall(callingPerson, declineIntent,
+                                answerIntent)
+                        .setVerificationIcon(verificationIcon))
+                .setContentTitle("Calling...")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .build();
+
+        Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+        n.visitUris(visitor);
+
+        verify(visitor, times(1)).accept(eq(personIcon.getUri()));
+        verify(visitor, times(1)).accept(eq(verificationIcon.getUri()));
+        verify(visitor, times(1)).accept(eq(answerIntent.getIntent().getData()));
+        verify(visitor, times(1)).accept(eq(declineUri));
     }
 
     @Test
@@ -6385,23 +6431,80 @@
     }
 
     @Test
+    @EnableFlags({android.app.Flags.FLAG_VISIT_RISKY_URIS})
     public void testVisitUris_wearableExtender() {
         Icon actionIcon = Icon.createWithContentUri("content://media/action");
         Icon wearActionIcon = Icon.createWithContentUri("content://media/wearAction");
-        PendingIntent intent = PendingIntent.getActivity(mContext, 0, new Intent(),
-                PendingIntent.FLAG_IMMUTABLE);
+        Uri displayIntentUri = Uri.parse("content://intent/display");
+        PendingIntent displayIntent = getPendingIntentWithUri(displayIntentUri);
+        Uri actionIntentUri = Uri.parse("content://intent/action");
+        PendingIntent actionIntent = getPendingIntentWithUri(actionIntentUri);
+        Uri wearActionIntentUri = Uri.parse("content://intent/wear");
+        PendingIntent wearActionIntent = getPendingIntentWithUri(wearActionIntentUri);
         Notification n = new Notification.Builder(mContext, "a")
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
-                .addAction(new Notification.Action.Builder(actionIcon, "Hey!", intent).build())
-                .extend(new Notification.WearableExtender().addAction(
-                        new Notification.Action.Builder(wearActionIcon, "Wear!", intent).build()))
+                .addAction(
+                        new Notification.Action.Builder(actionIcon, "Hey!", actionIntent).build())
+                .extend(new Notification.WearableExtender()
+                        .setDisplayIntent(displayIntent)
+                        .addAction(new Notification.Action.Builder(wearActionIcon, "Wear!",
+                                wearActionIntent)
+                                .build()))
                 .build();
 
         Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
         n.visitUris(visitor);
 
         verify(visitor).accept(eq(actionIcon.getUri()));
+        verify(visitor, times(1)).accept(eq(actionIntentUri));
         verify(visitor).accept(eq(wearActionIcon.getUri()));
+        verify(visitor, times(1)).accept(eq(wearActionIntentUri));
+    }
+
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_VISIT_RISKY_URIS})
+    public void testVisitUris_tvExtender() {
+        Uri contentIntentUri = Uri.parse("content://intent/content");
+        PendingIntent contentIntent = getPendingIntentWithUri(contentIntentUri);
+        Uri deleteIntentUri = Uri.parse("content://intent/delete");
+        PendingIntent deleteIntent = getPendingIntentWithUri(deleteIntentUri);
+        Notification n = new Notification.Builder(mContext, "a")
+                .extend(
+                        new Notification.TvExtender()
+                                .setContentIntent(contentIntent)
+                                .setDeleteIntent(deleteIntent))
+                .build();
+
+        Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+        n.visitUris(visitor);
+
+        verify(visitor, times(1)).accept(eq(contentIntentUri));
+        verify(visitor, times(1)).accept(eq(deleteIntentUri));
+    }
+
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_VISIT_RISKY_URIS})
+    public void testVisitUris_carExtender() {
+        final String testParticipant = "testParticipant";
+        Uri readPendingIntentUri = Uri.parse("content://intent/read");
+        PendingIntent readPendingIntent = getPendingIntentWithUri(readPendingIntentUri);
+        Uri replyPendingIntentUri = Uri.parse("content://intent/reply");
+        PendingIntent replyPendingIntent = getPendingIntentWithUri(replyPendingIntentUri);
+        final RemoteInput testRemoteInput = new RemoteInput.Builder("key").build();
+
+        Notification n = new Notification.Builder(mContext, "a")
+                .extend(new Notification.CarExtender().setUnreadConversation(
+                        new Notification.CarExtender.Builder(testParticipant)
+                                .setReadPendingIntent(readPendingIntent)
+                                .setReplyAction(replyPendingIntent, testRemoteInput)
+                                .build()))
+                .build();
+
+        Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+        n.visitUris(visitor);
+
+        verify(visitor, times(1)).accept(eq(readPendingIntentUri));
+        verify(visitor, times(1)).accept(eq(replyPendingIntentUri));
     }
 
     @Test
@@ -8951,7 +9054,7 @@
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_NONE, isEnabled);
 
         try {
-            mBinderService.addAutomaticZenRule(rule, mContext.getPackageName());
+            mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false);
             fail("Zen policy only applies to priority only mode");
         } catch (IllegalArgumentException e) {
             // yay
@@ -8959,11 +9062,11 @@
 
         rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName());
+        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false);
 
         rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 null, NotificationManager.INTERRUPTION_FILTER_NONE, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName());
+        mBinderService.addAutomaticZenRule(rule, mContext.getPackageName(), false);
     }
 
     @Test
@@ -8978,7 +9081,7 @@
         boolean isEnabled = true;
         AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, "com.android.settings");
+        mBinderService.addAutomaticZenRule(rule, "com.android.settings", false);
 
         // verify that zen mode helper gets passed in a package name of "android"
         verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule),
@@ -9000,7 +9103,7 @@
         boolean isEnabled = true;
         AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, "com.android.settings");
+        mBinderService.addAutomaticZenRule(rule, "com.android.settings", false);
 
         // verify that zen mode helper gets passed in a package name of "android"
         verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule),
@@ -9020,7 +9123,7 @@
         boolean isEnabled = true;
         AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
                 zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
-        mBinderService.addAutomaticZenRule(rule, "another.package");
+        mBinderService.addAutomaticZenRule(rule, "another.package", false);
 
         // verify that zen mode helper gets passed in the package name from the arg, not the owner
         verify(mockZenModeHelper).addAutomaticZenRule(
@@ -9031,10 +9134,8 @@
     @Test
     @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testAddAutomaticZenRule_typeManagedCanBeUsedByDeviceOwners() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
         mService.setCallerIsNormalPackage();
-        mService.setZenHelper(mock(ZenModeHelper.class));
-        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
-                .thenReturn(true);
 
         AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri"))
                 .setType(AutomaticZenRule.TYPE_MANAGED)
@@ -9042,8 +9143,9 @@
                 .build();
         when(mDevicePolicyManager.isActiveDeviceOwner(anyInt())).thenReturn(true);
 
-        mBinderService.addAutomaticZenRule(rule, "pkg");
-        // No exception!
+        mBinderService.addAutomaticZenRule(rule, "pkg", /* fromUser= */ false);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(rule), anyInt(), any(), anyInt());
     }
 
     @Test
@@ -9061,7 +9163,149 @@
         when(mDevicePolicyManager.isActiveDeviceOwner(anyInt())).thenReturn(false);
 
         assertThrows(IllegalArgumentException.class,
-                () -> mBinderService.addAutomaticZenRule(rule, "pkg"));
+                () -> mBinderService.addAutomaticZenRule(rule, "pkg", /* fromUser= */ false));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromUser_mappedToOriginUser() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromSystemNotUser_mappedToOriginSystem() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromApp_mappedToOriginApp() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false);
+
+        verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void addAutomaticZenRule_fromAppFromUser_blocked() throws Exception {
+        setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        assertThrows(SecurityException.class, () ->
+                mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.updateAutomaticZenRule("id", SOME_ZEN_RULE, /* fromUser= */ true);
+
+        verify(zenModeHelper).updateAutomaticZenRule(eq("id"), eq(SOME_ZEN_RULE),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void updateAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
+        setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        assertThrows(SecurityException.class, () ->
+                mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void removeAutomaticZenRule_fromUserFromSystem_allowed() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true);
+
+        verify(zenModeHelper).removeAutomaticZenRule(eq("id"),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void removeAutomaticZenRule_fromUserFromApp_blocked() throws Exception {
+        setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        assertThrows(SecurityException.class, () ->
+                mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true));
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void setAutomaticZenRuleState_conditionFromUser_mappedToOriginUser() throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        Condition withSourceUser = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_USER_ACTION);
+        mBinderService.setAutomaticZenRuleState("id", withSourceUser);
+
+        verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceUser),
+                eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void setAutomaticZenRuleState_fromAppWithConditionNotFromUser_mappedToOriginApp()
+            throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.setCallerIsNormalPackage();
+
+        Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_CONTEXT);
+        mBinderService.setAutomaticZenRuleState("id", withSourceContext);
+
+        verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext),
+                eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
+    public void setAutomaticZenRuleState_fromSystemWithConditionNotFromUser_mappedToOriginSystem()
+            throws Exception {
+        ZenModeHelper zenModeHelper = setUpMockZenTest();
+        mService.isSystemUid = true;
+
+        Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE,
+                SOURCE_CONTEXT);
+        mBinderService.setAutomaticZenRuleState("id", withSourceContext);
+
+        verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext),
+                eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyInt());
+    }
+
+    private ZenModeHelper setUpMockZenTest() {
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.setZenHelper(zenModeHelper);
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        return zenModeHelper;
     }
 
     @Test
@@ -9087,7 +9331,7 @@
         });
 
         mService.getBinderService().setZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS, null,
-                "testing!");
+                "testing!", false);
         waitForIdle();
 
         InOrder inOrder = inOrder(mContext);
@@ -11535,8 +11779,8 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testGetActiveNotificationsFromListener_redactNotification() throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         NotificationRecord r =
                 generateNotificationRecord(mTestNotificationChannel, 0, 0);
         mService.addNotification(r);
@@ -11565,12 +11809,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
     public void testGetSnoozedNotificationsFromListener_redactNotification() throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
         NotificationRecord r =
                 generateNotificationRecord(mTestNotificationChannel, 0, 0);
-        mService.addNotification(r);
-        mService.snoozeNotificationInt(r.getKey(), 1000, null, mListener);
+        when(mSnoozeHelper.getSnoozed()).thenReturn(List.of(r));
         when(mListeners.isUidTrusted(anyInt())).thenReturn(false);
         when(mListeners.hasSensitiveContent(any())).thenReturn(true);
         StatusBarNotification redacted = generateRedactedSbn(mTestNotificationChannel, 1, 1);
@@ -11750,6 +11993,97 @@
     }
 
     @Test
+    public void testMakeRankingUpdate_redactsIfRecordSensitiveAndServiceUntrusted() {
+        mSetFlagsRule.enableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
+        when(mListeners.isUidTrusted(anyInt())).thenReturn(false);
+        when(mListeners.hasSensitiveContent(any())).thenReturn(true);
+        NotificationRecord pkgA = new NotificationRecord(mContext,
+                generateSbn("a", 1000, 9, 0), mTestNotificationChannel);
+        addSmartActionsAndReplies(pkgA);
+        mService.addNotification(pkgA);
+        NotificationRecord pkgB = new NotificationRecord(mContext,
+                generateSbn("b", 1001, 9, 0), mTestNotificationChannel);
+        addSmartActionsAndReplies(pkgB);
+        mService.addNotification(pkgB);
+
+        ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
+        when(info.enabledAndUserMatches(anyInt())).thenReturn(true);
+        when(info.isSameUser(anyInt())).thenReturn(true);
+        NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(info);
+        NotificationListenerService.Ranking ranking =
+                nru.getRankingMap().getRawRankingObject(pkgA.getSbn().getKey());
+        assertEquals(0, ranking.getSmartActions().size());
+        assertEquals(0, ranking.getSmartReplies().size());
+        NotificationListenerService.Ranking ranking2 =
+                nru.getRankingMap().getRawRankingObject(pkgB.getSbn().getKey());
+        assertEquals(0, ranking2.getSmartActions().size());
+        assertEquals(0, ranking2.getSmartReplies().size());
+    }
+
+    @Test
+    public void testMakeRankingUpdate_doestntRedactIfFlagDisabled() {
+        mSetFlagsRule.disableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
+        when(mListeners.isUidTrusted(anyInt())).thenReturn(false);
+        when(mListeners.hasSensitiveContent(any())).thenReturn(true);
+        NotificationRecord pkgA = new NotificationRecord(mContext,
+                generateSbn("a", 1000, 9, 0), mTestNotificationChannel);
+        addSmartActionsAndReplies(pkgA);
+
+        mService.addNotification(pkgA);
+        ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
+        when(info.enabledAndUserMatches(anyInt())).thenReturn(true);
+        when(info.isSameUser(anyInt())).thenReturn(true);
+        NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(info);
+        NotificationListenerService.Ranking ranking =
+                nru.getRankingMap().getRawRankingObject(pkgA.getSbn().getKey());
+        assertEquals(1, ranking.getSmartActions().size());
+        assertEquals(1, ranking.getSmartReplies().size());
+    }
+
+    @Test
+    public void testMakeRankingUpdate_doesntRedactIfNotSensitiveOrServiceTrusted() {
+        mSetFlagsRule.disableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS);
+        NotificationRecord pkgA = new NotificationRecord(mContext,
+                generateSbn("a", 1000, 9, 0), mTestNotificationChannel);
+        addSmartActionsAndReplies(pkgA);
+
+        mService.addNotification(pkgA);
+        ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class);
+        when(info.enabledAndUserMatches(anyInt())).thenReturn(true);
+        when(info.isSameUser(anyInt())).thenReturn(true);
+
+        // No sensitive content, no redaction
+        when(mListeners.isUidTrusted(eq(1000))).thenReturn(false);
+        when(mListeners.hasSensitiveContent(any())).thenReturn(false);
+        NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(info);
+        NotificationListenerService.Ranking ranking =
+                nru.getRankingMap().getRawRankingObject(pkgA.getSbn().getKey());
+        assertEquals(1, ranking.getSmartActions().size());
+        assertEquals(1, ranking.getSmartReplies().size());
+
+        // trusted listener, no redaction
+        when(mListeners.isUidTrusted(eq(1000))).thenReturn(true);
+        when(mListeners.hasSensitiveContent(any())).thenReturn(true);
+        nru = mService.makeRankingUpdateLocked(info);
+        ranking = nru.getRankingMap().getRawRankingObject(pkgA.getSbn().getKey());
+        assertEquals(1, ranking.getSmartActions().size());
+        assertEquals(1, ranking.getSmartReplies().size());
+    }
+
+    private void addSmartActionsAndReplies(NotificationRecord record) {
+        Bundle b = new Bundle();
+        ArrayList<Notification.Action> actions = new ArrayList<>();
+        actions.add(new Notification.Action(0, "", null));
+        b.putParcelableArrayList(KEY_CONTEXTUAL_ACTIONS, actions);
+        ArrayList<CharSequence> replies = new ArrayList<>(List.of("test"));
+        b.putCharSequenceArrayList(KEY_TEXT_REPLIES, replies);
+        Adjustment a = new Adjustment(record.getSbn().getPackageName(), record.getSbn().getKey(),
+                b, "", record.getUserId());
+        record.addAdjustment(a);
+        record.applyAdjustments();
+    }
+
+    @Test
     public void testMaybeShowReviewPermissionsNotification_flagOff() {
         mService.setShowReviewPermissionsNotification(false);
         reset(mMockNm);
@@ -12060,7 +12394,9 @@
                 /* isImageBitmap= */ true,
                 /* isExpired= */ true);
         addRecordAndRemoveBitmaps(record);
-        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE)).isFalse();
+        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE)).isTrue();
+        final Parcelable picture = record.getNotification().extras.getParcelable(EXTRA_PICTURE);
+        assertThat(picture).isNull();
     }
 
     @Test
@@ -12094,7 +12430,10 @@
                 /* isImageBitmap= */ false,
                 /* isExpired= */ true);
         addRecordAndRemoveBitmaps(record);
-        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE_ICON)).isFalse();
+        assertThat(record.getNotification().extras.containsKey(EXTRA_PICTURE_ICON)).isTrue();
+        final Parcelable pictureIcon =
+                record.getNotification().extras.getParcelable(EXTRA_PICTURE_ICON);
+        assertThat(pictureIcon).isNull();
     }
 
     @Test
@@ -13344,9 +13683,10 @@
                 .thenReturn(true);
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
-        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy));
+        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy),
+                eq(ZenModeConfig.UPDATE_ORIGIN_APP));
     }
 
     @Test
@@ -13360,14 +13700,38 @@
         mService.isSystemUid = true;
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
         verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
     }
 
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
-    public void setNotificationPolicy_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+    public void setNotificationPolicy_watchCompanionApp_setsGlobalPolicy()
+            throws RemoteException {
+        setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy(
+                AssociationRequest.DEVICE_PROFILE_WATCH, true);
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_autoCompanionApp_setsGlobalPolicy()
+            throws RemoteException {
+        setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy(
+                AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, true);
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_otherCompanionApp_doesNotSetGlobalPolicy()
+            throws RemoteException {
+        setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy(
+                AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, false);
+    }
+
+    private void setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy(
+            @AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy)
+            throws RemoteException {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mService.setCallerIsNormalPackage();
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
@@ -13377,14 +13741,19 @@
         when(mCompanionMgr.getAssociations(anyString(), anyInt()))
                 .thenReturn(ImmutableList.of(
                         new AssociationInfo.Builder(1, mUserId, "package")
-                                .setDisplayName("My watch")
-                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .setDisplayName("My connected device")
+                                .setDeviceProfile(deviceProfile)
                                 .build()));
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
-        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
+        if (canSetGlobalPolicy) {
+            verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
+        } else {
+            verify(zenModeHelper).applyGlobalPolicyAsImplicitZenRule(anyString(), anyInt(),
+                    eq(policy), anyInt());
+        }
     }
 
     @Test
@@ -13398,7 +13767,7 @@
                 .thenReturn(true);
 
         NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
-        mBinderService.setNotificationPolicy("package", policy);
+        mBinderService.setNotificationPolicy("package", policy, false);
 
         verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
     }
@@ -13428,7 +13797,7 @@
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
                 .thenReturn(true);
 
-        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false);
 
         verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(),
                 eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
@@ -13445,7 +13814,7 @@
                 .thenReturn(true);
         mService.isSystemUid = true;
 
-        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false);
 
         verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
                 eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), eq("package"),
@@ -13454,7 +13823,29 @@
 
     @Test
     @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
-    public void setInterruptionFilter_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+    public void setInterruptionFilter_watchCompanionApp_setsGlobalZen() throws RemoteException {
+        setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen(
+                AssociationRequest.DEVICE_PROFILE_WATCH, true);
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_autoCompanionApp_setsGlobalZen() throws RemoteException {
+        setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen(
+                AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, true);
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_otherCompanionApp_doesNotSetGlobalZen()
+            throws RemoteException {
+        setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen(
+                AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING, false);
+    }
+
+    private void setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen(
+            @AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy)
+            throws RemoteException {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
         mService.mZenModeHelper = zenModeHelper;
@@ -13464,14 +13855,19 @@
         when(mCompanionMgr.getAssociations(anyString(), anyInt()))
                 .thenReturn(ImmutableList.of(
                         new AssociationInfo.Builder(1, mUserId, "package")
-                                .setDisplayName("My watch")
-                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .setDisplayName("My connected device")
+                                .setDeviceProfile(deviceProfile)
                                 .build()));
 
-        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false);
 
-        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
-                eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), eq("package"), anyInt());
+        if (canSetGlobalPolicy) {
+            verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                    eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), eq("package"), anyInt());
+        } else {
+            verify(zenModeHelper).applyGlobalZenModeAsImplicitZenRule(anyString(), anyInt(),
+                    eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+        }
     }
 
     @Test
@@ -13489,6 +13885,7 @@
     }
 
     @Test
+    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne()
             throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -13518,6 +13915,7 @@
     }
 
     @Test
+    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                 .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
@@ -13545,6 +13943,7 @@
     }
 
     @Test
+    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled()
             throws RemoteException {
         mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -13575,6 +13974,7 @@
     }
 
     @Test
+    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll()
             throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -13603,6 +14003,7 @@
     }
 
     @Test
+    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                 .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
@@ -13629,6 +14030,7 @@
     }
 
     @Test
+    @Ignore("b/316989461")
     public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled()
             throws RemoteException {
         mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 44dbe385..ea948ca 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -146,6 +146,10 @@
                     .put(Notification.Action.Builder.class, "extend")
                     // Overwrites icon supplied to constructor.
                     .put(Notification.BubbleMetadata.Builder.class, "setIcon")
+                    // Overwrites intent supplied to constructor.
+                    .put(Notification.BubbleMetadata.Builder.class, "setIntent")
+                    // Overwrites intent supplied to constructor.
+                    .put(Notification.BubbleMetadata.Builder.class, "setDeleteIntent")
                     // Discards previously-added actions.
                     .put(RemoteViews.class, "mergeRemoteViews")
                     .build();
@@ -680,14 +684,14 @@
             }
 
             if (clazz == Intent.class) {
-                // TODO(b/281044385): Are Intent Uris (new Intent(String,Uri)) relevant?
-                return new Intent("action");
+                return new Intent("action", generateUri(where.plus(Intent.class)));
             }
 
             if (clazz == PendingIntent.class) {
-                // PendingIntent can have an Intent with a Uri but those are inaccessible and
-                // not inspected.
-                return PendingIntent.getActivity(mContext, 0, new Intent("action"),
+                // PendingIntent can have an Intent with a Uri.
+                Uri intentUri = generateUri(where.plus(PendingIntent.class));
+                return PendingIntent.getActivity(mContext, 0,
+                        new Intent("action", intentUri),
                         PendingIntent.FLAG_IMMUTABLE);
             }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
index 4a1435f..1fcee06 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
@@ -99,7 +99,7 @@
     public boolean getFromSystemOrSystemUi(int i) throws IllegalArgumentException {
         // While this isn't a logged output value, it's still helpful to check in tests.
         checkInRange(i);
-        return mChanges.get(i).mFromSystemOrSystemUi;
+        return mChanges.get(i).isFromSystemOrSystemUi();
     }
 
     public boolean getIsUserAction(int i) throws IllegalArgumentException {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 1aea56c..44f0894 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -43,6 +43,8 @@
 import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_SCHEDULE;
+import static android.service.notification.Condition.SOURCE_USER_ACTION;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP;
@@ -112,6 +114,7 @@
 import android.os.Parcel;
 import android.os.Process;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.Settings.Global;
@@ -119,12 +122,13 @@
 import android.service.notification.DeviceEffectsApplier;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ConfigChangeOrigin;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.service.notification.ZenModeConfig.ZenRule;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
+import android.testing.TestWithLooperRule;
 import android.testing.TestableLooper;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -147,6 +151,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.truth.Correspondence;
 import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -172,7 +178,7 @@
 
 @SmallTest
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
-@RunWith(AndroidTestingRunner.class)
+@RunWith(TestParameterInjector.class)
 @TestableLooper.RunWithLooper
 public class ZenModeHelperTest extends UiServiceTestCase {
 
@@ -211,7 +217,11 @@
     private static final ZenDeviceEffects NO_EFFECTS = new ZenDeviceEffects.Builder().build();
 
     @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+            SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+    @Rule(order = Integer.MAX_VALUE) // set the highest order so it's the innermost rule
+    public TestWithLooperRule mLooperRule = new TestWithLooperRule();
 
     ConditionProviders mConditionProviders;
     @Mock NotificationManager mNotificationManager;
@@ -2339,15 +2349,38 @@
         assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
     }
 
+    private enum ModesApiFlag {
+        ENABLED(true, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
+        DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
+
+        private final boolean mEnabled;
+        @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi;
+
+        ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
+            this.mEnabled = enabled;
+            this.mOriginForUserActionInSystemUi = originForUserActionInSystemUi;
+        }
+
+        void applyFlag(SetFlagsRule setFlagsRule) {
+            if (mEnabled) {
+                setFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+            } else {
+                setFlagsRule.disableFlags(Flags.FLAG_MODES_API);
+            }
+        }
+    }
+
     @Test
-    public void testZenModeEventLog_setManualZenMode() throws IllegalArgumentException {
+    public void testZenModeEventLog_setManualZenMode(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
         // Turn zen mode on (to important_interruptions)
         // Need to additionally call the looper in order to finish the post-apply-config process
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
@@ -2374,7 +2407,8 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
-        assertTrue(mZenModeEventLogger.getFromSystemOrSystemUi(0));
+        assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo(
+                modesApiFlag == ModesApiFlag.DISABLED);
         assertTrue(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
@@ -2399,7 +2433,9 @@
     }
 
     @Test
-    public void testZenModeEventLog_automaticRules() throws IllegalArgumentException {
+    public void testZenModeEventLog_automaticRules(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2421,8 +2457,8 @@
 
         // Event 2: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
-                Process.SYSTEM_UID);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule,
+                modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
 
         // Add a new system rule
         AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -2440,8 +2476,8 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Event 4: "User" deletes the rule
-        mZenModeHelper.removeAutomaticZenRule(systemId, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
-                Process.SYSTEM_UID);
+        mZenModeHelper.removeAutomaticZenRule(systemId, modesApiFlag.mOriginForUserActionInSystemUi,
+                "", Process.SYSTEM_UID);
 
         // In total, this represents 4 events
         assertEquals(4, mZenModeEventLogger.numLoggedChanges());
@@ -2497,20 +2533,109 @@
     }
 
     @Test
-    public void testZenModeEventLog_policyChanges() throws IllegalArgumentException {
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void testZenModeEventLog_automaticRuleActivatedFromAppByAppAndUser()
+            throws IllegalArgumentException {
+        mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
+        setupZenConfig();
+
+        // Ann app adds an automatic zen rule
+        AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                null,
+                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                null,
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+        String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+                UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID);
+
+        // Event 1: Mimic the rule coming on manually when the user turns it on in the app
+        // ("Turn on bedtime now" because user goes to bed earlier).
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_USER_ACTION),
+                UPDATE_ORIGIN_USER, CUSTOM_PKG_UID);
+
+        // Event 2: App deactivates the rule automatically (it's 8 AM, bedtime schedule ends)
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE),
+                UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
+
+        // Event 3: App activates the rule automatically (it's now 11 PM, bedtime schedule starts)
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE),
+                UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
+
+        // Event 4: User deactivates the rule manually (they get up before 8 AM on the next day)
+        mZenModeHelper.setAutomaticZenRuleState(id,
+                new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_USER_ACTION),
+                UPDATE_ORIGIN_USER, CUSTOM_PKG_UID);
+
+        // In total, this represents 4 events
+        assertEquals(4, mZenModeEventLogger.numLoggedChanges());
+
+        // Automatic rule turning on manually:
+        //   - event ID: DND_TURNED_ON
+        //   - 1 rule (newly) active
+        //   - is a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
+                mZenModeEventLogger.getEventId(0));
+        assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
+        assertTrue(mZenModeEventLogger.getIsUserAction(0));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
+
+        // Automatic rule turned off automatically by app:
+        //   - event ID: DND_TURNED_OFF
+        //   - 0 rules active
+        //   - is not a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
+                mZenModeEventLogger.getEventId(1));
+        assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
+        assertFalse(mZenModeEventLogger.getIsUserAction(1));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
+
+        // Automatic rule turned on automatically by app:
+        //   - event ID: DND_TURNED_ON
+        //   - 1 rule (newly) active
+        //   - is not a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
+                mZenModeEventLogger.getEventId(2));
+        assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2));
+        assertEquals(1, mZenModeEventLogger.getNumRulesActive(2));
+        assertFalse(mZenModeEventLogger.getIsUserAction(2));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(2));
+
+        // Automatic rule turned off automatically by the user:
+        //   - event ID: DND_TURNED_ON
+        //   - 0 rules active
+        //   - is a user action
+        //   - package UID is the calling package
+        assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
+                mZenModeEventLogger.getEventId(3));
+        assertEquals(0, mZenModeEventLogger.getNumRulesActive(3));
+        assertTrue(mZenModeEventLogger.getIsUserAction(3));
+        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3));
+    }
+
+    @Test
+    public void testZenModeEventLog_policyChanges(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
         // First just turn zen mode on
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
 
         // Now change the policy slightly; want to confirm that this'll be reflected in the logs
         ZenModeConfig newConfig = mZenModeHelper.mConfig.copy();
         newConfig.allowAlarms = true;
         newConfig.allowRepeatCallers = false;
         mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
 
         // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
         // is off.
@@ -2521,7 +2646,7 @@
         newConfig.allowMessages = false;
         newConfig.allowRepeatCallers = true;
         mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
 
         // Total events: we only expect ones for turning on, changing policy, and turning off
         assertEquals(3, mZenModeEventLogger.numLoggedChanges());
@@ -2554,7 +2679,9 @@
     }
 
     @Test
-    public void testZenModeEventLog_ruleCounts() throws IllegalArgumentException {
+    public void testZenModeEventLog_ruleCounts(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2657,8 +2784,10 @@
     }
 
     @Test
-    public void testZenModeEventLog_noLogWithNoConfigChange() throws IllegalArgumentException {
+    public void testZenModeEventLog_noLogWithNoConfigChange(
+            @TestParameter ModesApiFlag modesApiFlag) throws IllegalArgumentException {
         // If evaluateZenMode is called independently of a config change, don't log.
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2675,9 +2804,11 @@
     }
 
     @Test
-    public void testZenModeEventLog_reassignUid() throws IllegalArgumentException {
+    public void testZenModeEventLog_reassignUid(@TestParameter ModesApiFlag modesApiFlag)
+            throws IllegalArgumentException {
         // Test that, only in specific cases, we reassign the calling UID to one associated with
         // the automatic rule owner.
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2689,7 +2820,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+                UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID);
 
         // Rule 2, same as rule 1 but owned by the system
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -2699,11 +2830,11 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+                modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
 
         // Turn on rule 1; call looks like it's from the system. Because setting a condition is
         // typically an automatic (non-user-initiated) action, expect the calling UID to be
-        // re-evaluated to the one associat.d with CUSTOM_PKG_NAME.
+        // re-evaluated to the one associated with CUSTOM_PKG_NAME.
         mZenModeHelper.setAutomaticZenRuleState(id,
                 new Condition(zenRule.getConditionId(), "", STATE_TRUE),
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
@@ -2717,8 +2848,8 @@
         // Disable rule 1. Because this looks like a user action, the UID should not be modified
         // from the system-provided one.
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
-                Process.SYSTEM_UID);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule,
+                modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
 
         // Add a manual rule. Any manual rule changes should not get calling uids reassigned.
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP,
@@ -2775,8 +2906,10 @@
     }
 
     @Test
-    public void testZenModeEventLog_channelsBypassingChanges() {
+    public void testZenModeEventLog_channelsBypassingChanges(
+            @TestParameter ModesApiFlag modesApiFlag) {
         // Verify that the right thing happens when the canBypassDnd value changes.
+        modesApiFlag.applyFlag(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -2872,14 +3005,11 @@
         // Second message where we change the policy:
         //   - DND_POLICY_CHANGED (indicates only the policy changed and nothing else)
         //   - rule type: unknown (it's a policy change, not a rule change)
-        //   - user action (because it comes from a "system" uid)
         //   - change is in allow channels, and final policy
         assertThat(mZenModeEventLogger.getEventId(1))
                 .isEqualTo(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId());
         assertThat(mZenModeEventLogger.getChangedRuleType(1))
                 .isEqualTo(DNDProtoEnums.UNKNOWN_RULE);
-        assertThat(mZenModeEventLogger.getIsUserAction(1)).isTrue();
-        assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo(Process.SYSTEM_UID);
         DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1);
         assertThat(dndProto.getAllowChannels().getNumber())
                 .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE);
@@ -3372,6 +3502,29 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() {
+        mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
+        reset(mDeviceEffectsApplier);
+
+        String ruleId = addRuleWithEffects(new ZenDeviceEffects.Builder()
+                .setShouldSuppressAmbientDisplay(true)
+                .setShouldDimWallpaper(true)
+                .build());
+        mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        mTestableLooper.processAllMessages();
+        verify(mDeviceEffectsApplier).apply(any(), eq(UPDATE_ORIGIN_APP));
+
+        // Now delete the (currently active!) rule. For example, assume this is done from settings.
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "remove",
+                Process.SYSTEM_UID);
+        mTestableLooper.processAllMessages();
+
+        verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_USER));
+    }
+
+    @Test
     public void testDeviceEffects_applied() {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier);
@@ -3511,8 +3664,8 @@
         AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
                 .setDeviceEffects(effects)
                 .build();
-        return mZenModeHelper.addAutomaticZenRule("pkg", rule, UPDATE_ORIGIN_APP, "",
-                CUSTOM_PKG_UID);
+        return mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
     }
 
     @Test
@@ -3619,7 +3772,8 @@
         Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
                 PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
                 Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
-        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy,
+                UPDATE_ORIGIN_APP);
 
         ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
                 .disallowAllSounds()
@@ -3643,13 +3797,14 @@
                 PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
                 Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
-                original);
+                original, UPDATE_ORIGIN_APP);
 
         // Change priorityCallSenders: contacts -> starred.
         Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
                 PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED,
                 Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
-        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated,
+                UPDATE_ORIGIN_APP);
 
         ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
                 .disallowAllSounds()
@@ -3671,7 +3826,7 @@
 
         withoutWtfCrash(
                 () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME,
-                        CUSTOM_PKG_UID, new Policy(0, 0, 0)));
+                        CUSTOM_PKG_UID, new Policy(0, 0, 0), UPDATE_ORIGIN_APP));
 
         assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
     }
@@ -3684,7 +3839,7 @@
                 Policy.getAllSuppressedVisualEffects(), STATE_FALSE,
                 CONVERSATION_SENDERS_IMPORTANT);
         mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
-                writtenPolicy);
+                writtenPolicy, UPDATE_ORIGIN_APP);
 
         Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
                 CUSTOM_PKG_NAME);
diff --git a/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java b/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java
index d2ef180..ca3787e 100644
--- a/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java
@@ -95,6 +95,26 @@
         assertFalse(action.executed);
     }
 
+    @Test
+    public void queueKeyAction_beforeAndAfterCancelQueuedActions_onlyActionsAfterCancelExecuted() {
+        TestAction action1 = new TestAction();
+        TestAction action2 = new TestAction();
+        TestAction action3 = new TestAction();
+        mKeyActionExecutor.queueKeyAction(
+                KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action1);
+        mKeyActionExecutor.queueKeyAction(
+                KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action2);
+        mKeyActionExecutor.cancelQueuedAction(KeyEvent.KEYCODE_STEM_PRIMARY);
+        mKeyActionExecutor.queueKeyAction(
+                KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action3);
+
+        mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1);
+
+        assertFalse(action1.executed);
+        assertFalse(action2.executed);
+        assertTrue(action3.executed);
+    }
+
     static class TestAction implements Runnable {
         public boolean executed;
 
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index f7ad2a8..50d37ec 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -19,14 +19,18 @@
 import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS;
 import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_LONG_PRESS;
 import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_SHORT_PRESS;
+import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS;
 import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT;
 import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
 import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_TARGET_ACTIVITY;
+import static com.android.server.policy.PhoneWindowManager.TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY;
 
 import android.app.ActivityManager.RecentTaskInfo;
+import android.app.ActivityTaskManager.RootTaskInfo;
 import android.content.ComponentName;
 import android.os.RemoteException;
 import android.provider.Settings;
@@ -50,6 +54,7 @@
     public void stemSingleKey_duringSetup_doNothing() {
         overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
         mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
         mPhoneWindowManager.overrideIsUserSetupComplete(false);
 
@@ -65,6 +70,7 @@
     public void stemSingleKey_AfterSetup_openAllApp() {
         overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
         mPhoneWindowManager.overrideStartActivity();
         mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
         mPhoneWindowManager.overrideIsUserSetupComplete(true);
@@ -83,6 +89,7 @@
                 STEM_PRIMARY_BUTTON_SHORT_PRESS,
                 SHORT_PRESS_PRIMARY_LAUNCH_TARGET_ACTIVITY);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
         mPhoneWindowManager.overrideStartActivity();
         mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
         mPhoneWindowManager.overrideIsUserSetupComplete(true);
@@ -104,6 +111,7 @@
         mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
         mPhoneWindowManager.overrideIsUserSetupComplete(true);
         mPhoneWindowManager.overrideFocusedWindowButtonOverridePermission(true);
+
         setDispatchedKeyHandler(keyEvent -> true);
 
         sendKey(KEYCODE_STEM_PRIMARY);
@@ -131,6 +139,7 @@
                 STEM_PRIMARY_BUTTON_LONG_PRESS,
                 LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
         mPhoneWindowManager.setupAssistForLaunch();
         mPhoneWindowManager.overrideIsUserSetupComplete(true);
 
@@ -144,6 +153,7 @@
                 STEM_PRIMARY_BUTTON_LONG_PRESS,
                 LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
         mPhoneWindowManager.setupAssistForLaunch();
         mPhoneWindowManager.overrideSearchManager(null);
         mPhoneWindowManager.overrideStatusBarManagerInternal();
@@ -156,7 +166,8 @@
     @Test
     public void stemDoubleKey_EarlyShortPress_AllAppsThenSwitchToMostRecent()
             throws RemoteException {
-        overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
+        overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
+        overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
         mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(true);
         mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
@@ -171,14 +182,47 @@
         sendKey(KEYCODE_STEM_PRIMARY);
 
         mPhoneWindowManager.assertOpenAllAppView();
-        mPhoneWindowManager.assertSwitchToRecent(referenceId);
+        mPhoneWindowManager.assertSwitchToTask(referenceId);
     }
 
     @Test
-    public void stemDoubleKey_NoEarlyShortPress_SwitchToMostRecent() throws RemoteException {
+    public void stemTripleKey_EarlyShortPress_AllAppsThenBackToOriginalThenToggleA11y()
+            throws RemoteException {
+        overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
+        overrideBehavior(
+                STEM_PRIMARY_BUTTON_TRIPLE_PRESS, TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY);
+        setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(true);
+        mPhoneWindowManager.overrideTalkbackShortcutGestureEnabled(true);
+        mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+        mPhoneWindowManager.overrideIsUserSetupComplete(true);
+        RootTaskInfo allAppsTask = new RootTaskInfo();
+        int referenceId = 777;
+        allAppsTask.taskId = referenceId;
+        doReturn(allAppsTask)
+                .when(mPhoneWindowManager.mActivityManagerService)
+                .getFocusedRootTaskInfo();
+
+        mPhoneWindowManager.assertTalkBack(/* expectEnabled= */ false);
+
+        sendKey(KEYCODE_STEM_PRIMARY);
+        sendKey(KEYCODE_STEM_PRIMARY);
+        sendKey(KEYCODE_STEM_PRIMARY);
+
+        mPhoneWindowManager.assertOpenAllAppView();
+        mPhoneWindowManager.assertSwitchToTask(referenceId);
+        mPhoneWindowManager.assertTalkBack(/* expectEnabled= */ true);
+    }
+
+    @Test
+    public void stemMultiKey_NoEarlyPress_NoOpenAllApp() throws RemoteException {
+        overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
         overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
+        overrideBehavior(
+                STEM_PRIMARY_BUTTON_TRIPLE_PRESS, TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY);
         setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
         mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
+        mPhoneWindowManager.overrideTalkbackShortcutGestureEnabled(true);
         mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
         mPhoneWindowManager.overrideIsUserSetupComplete(true);
         RecentTaskInfo recentTaskInfo = new RecentTaskInfo();
@@ -189,9 +233,16 @@
 
         sendKey(KEYCODE_STEM_PRIMARY);
         sendKey(KEYCODE_STEM_PRIMARY);
+        sendKey(KEYCODE_STEM_PRIMARY);
 
         mPhoneWindowManager.assertNotOpenAllAppView();
-        mPhoneWindowManager.assertSwitchToRecent(referenceId);
+        mPhoneWindowManager.assertTalkBack(/* expectEnabled= */ true);
+
+        sendKey(KEYCODE_STEM_PRIMARY);
+        sendKey(KEYCODE_STEM_PRIMARY);
+
+        mPhoneWindowManager.assertNotOpenAllAppView();
+        mPhoneWindowManager.assertSwitchToTask(referenceId);
     }
 
     @Test
@@ -215,7 +266,7 @@
         sendKey(KEYCODE_STEM_PRIMARY);
 
         mPhoneWindowManager.assertNotOpenAllAppView();
-        mPhoneWindowManager.assertSwitchToRecent(referenceId);
+        mPhoneWindowManager.assertSwitchToTask(referenceId);
     }
 
     private void overrideBehavior(String key, int expectedBehavior) {
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 0678210..7c2f7ee 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -73,6 +73,7 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
@@ -176,8 +177,9 @@
     private Handler mHandler;
 
     private boolean mIsTalkBackEnabled;
+    private boolean mIsTalkBackShortcutGestureEnabled;
 
-    class TestTalkbackShortcutController extends TalkbackShortcutController {
+    private class TestTalkbackShortcutController extends TalkbackShortcutController {
         TestTalkbackShortcutController(Context context) {
             super(context);
         }
@@ -190,13 +192,18 @@
 
         @Override
         boolean isTalkBackShortcutGestureEnabled() {
-            return true;
+            return mIsTalkBackShortcutGestureEnabled;
         }
     }
 
     private class TestInjector extends PhoneWindowManager.Injector {
         TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
-            super(context, funcs, mTestLooper.getLooper());
+            super(context, funcs);
+        }
+
+        @Override
+        Looper getLooper() {
+            return mTestLooper.getLooper();
         }
 
         AccessibilityShortcutController getAccessibilityShortcutController(
@@ -410,6 +417,10 @@
         mPhoneWindowManager.mShouldEarlyShortPressOnStemPrimary = shouldEarlyShortPress;
     }
 
+    void overrideTalkbackShortcutGestureEnabled(boolean enabled) {
+        mIsTalkBackShortcutGestureEnabled = enabled;
+    }
+
      // Override assist perform function.
     void overrideLongPressOnPower(int behavior) {
         mPhoneWindowManager.mLongPressOnPowerBehavior = behavior;
@@ -714,7 +725,7 @@
     }
 
     void assertOpenAllAppView() {
-        mTestLooper.dispatchAll();
+        moveTimeForward(TEST_SINGLE_KEY_DELAY_MILLIS);
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext, timeout(TEST_SINGLE_KEY_DELAY_MILLIS))
                 .startActivityAsUser(intentCaptor.capture(), isNull(), any(UserHandle.class));
@@ -728,7 +739,7 @@
     }
 
     void assertActivityTargetLaunched(ComponentName targetActivity) {
-        mTestLooper.dispatchAll();
+        moveTimeForward(TEST_SINGLE_KEY_DELAY_MILLIS);
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext, timeout(TEST_SINGLE_KEY_DELAY_MILLIS))
                 .startActivityAsUser(intentCaptor.capture(), isNull(), any(UserHandle.class));
@@ -743,10 +754,15 @@
                         expectedModifierState, deviceBus), description(errorMsg));
     }
 
-    void assertSwitchToRecent(int persistentId) throws RemoteException {
+    void assertSwitchToTask(int persistentId) throws RemoteException {
         mTestLooper.dispatchAll();
         verify(mActivityManagerService,
                 timeout(TEST_SINGLE_KEY_DELAY_MILLIS)).startActivityFromRecents(eq(persistentId),
                 isNull());
     }
+
+    void assertTalkBack(boolean expectEnabled) {
+        mTestLooper.dispatchAll();
+        Assert.assertEquals(expectEnabled, mIsTalkBackEnabled);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 85c6f9e..718c598 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -202,8 +202,7 @@
     }
 
     private TestStartingWindowOrganizer registerTestStartingWindowOrganizer() {
-        return new TestStartingWindowOrganizer(mAtm,
-                mSystemServicesTestRule.getPowerManagerWrapper());
+        return new TestStartingWindowOrganizer(mAtm);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
index 526201f..670f9f6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
@@ -49,6 +49,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -195,7 +196,7 @@
         mAInfo.applicationInfo.flags = FLAG_SUSPENDED;
 
         when(mPackageManagerInternal.getSuspendingPackage(TEST_PACKAGE_NAME, TEST_USER_ID))
-                .thenReturn(PLATFORM_PACKAGE_NAME);
+                .thenReturn(UserPackage.of(TEST_USER_ID, PLATFORM_PACKAGE_NAME));
 
         // THEN calling intercept returns true
         assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null));
@@ -227,9 +228,10 @@
                 .setMessage("Test Message")
                 .setIcon(0x11110001)
                 .build();
+        UserPackage suspender = UserPackage.of(TEST_USER_ID, suspendingPackage);
         when(mPackageManagerInternal.getSuspendingPackage(TEST_PACKAGE_NAME, TEST_USER_ID))
-                .thenReturn(suspendingPackage);
-        when(mPackageManagerInternal.getSuspendedDialogInfo(TEST_PACKAGE_NAME, suspendingPackage,
+                .thenReturn(suspender);
+        when(mPackageManagerInternal.getSuspendedDialogInfo(TEST_PACKAGE_NAME, suspender,
                 TEST_USER_ID)).thenReturn(dialogInfo);
         return dialogInfo;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index d2c731c..ed99108 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -1087,4 +1087,16 @@
         assertTrue(homeActivity.getTask().isFocused());
         assertFalse(pinnedTask.isFocused());
     }
+
+    @Test
+    public void testContinueWindowLayout_notifyClientLifecycleManager() {
+        clearInvocations(mClientLifecycleManager);
+        mAtm.deferWindowLayout();
+
+        verify(mClientLifecycleManager, never()).onLayoutContinued();
+
+        mAtm.continueWindowLayout();
+
+        verify(mClientLifecycleManager).onLayoutContinued();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
index 7fdc5fc..c757457 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wm;
 
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -52,14 +54,12 @@
  * Build/Install/Run:
  *  atest WmTests:ClientLifecycleManagerTests
  */
-// Suppress GuardedBy warning on unit tests
-@SuppressWarnings("GuardedBy")
 @SmallTest
 @Presubmit
 public class ClientLifecycleManagerTests {
 
     @Rule(order = 0)
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
     @Rule(order = 1)
     public final SystemServicesTestRule mSystemServices = new SystemServicesTestRule();
@@ -225,4 +225,30 @@
         verify(mTransaction).schedule();
         verify(mTransaction).recycle();
     }
+
+    @Test
+    public void testLayoutDeferred() throws RemoteException {
+        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
+        spyOn(mWms.mWindowPlacerLocked);
+        doReturn(false).when(mWms.mWindowPlacerLocked).isInLayout();
+        doReturn(false).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
+        doReturn(true).when(mWms.mWindowPlacerLocked).isLayoutDeferred();
+
+        // Queue transactions during layout deferred.
+        mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mLifecycleItem);
+
+        verify(mLifecycleManager, never()).scheduleTransaction(any());
+
+        // Continue queueing when there are multi-level defer.
+        mLifecycleManager.onLayoutContinued();
+
+        verify(mLifecycleManager, never()).scheduleTransaction(any());
+
+        // Immediately dispatch when layout continue without ongoing/scheduled layout.
+        doReturn(false).when(mWms.mWindowPlacerLocked).isLayoutDeferred();
+
+        mLifecycleManager.onLayoutContinued();
+
+        verify(mLifecycleManager).scheduleTransaction(any());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java b/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java
index 1f7b65e..c187263 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CompatModePackagesTests.java
@@ -20,10 +20,13 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 
 import android.app.GameManagerInternal;
+import android.content.pm.ApplicationInfo;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
@@ -90,6 +93,14 @@
     public void testGetCompatScale_noGameManager() {
         assertEquals(mAtm.mCompatModePackages.getCompatScale(TEST_PACKAGE, TEST_USER_ID), 1f,
                 0.01f);
-    }
 
+        final ApplicationInfo info = new ApplicationInfo();
+        // Any non-zero value without FLAG_SUPPORTS_*_SCREENS.
+        info.flags = ApplicationInfo.FLAG_HAS_CODE;
+        info.packageName = info.sourceDir = "legacy.app";
+        mAtm.mCompatModePackages.compatibilityInfoForPackageLocked(info);
+        assertTrue(mAtm.mCompatModePackages.useLegacyScreenCompatMode(info.packageName));
+        mAtm.mCompatModePackages.handlePackageUninstalledLocked(info.packageName);
+        assertFalse(mAtm.mCompatModePackages.useLegacyScreenCompatMode(info.packageName));
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index c6796dc..985be42 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -19,6 +19,7 @@
 import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
 import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
@@ -673,6 +674,47 @@
     }
 
     @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser()
+            throws Exception {
+        mDisplayContent.setIgnoreOrientationRequest(true);
+        assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_returnsUnchanged()
+            throws Exception {
+        mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+        mDisplayContent.setIgnoreOrientationRequest(true);
+
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
+                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
+            throws Exception {
+        mDisplayContent.setIgnoreOrientationRequest(false);
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
+             /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_returnsUnchanged()
+            throws Exception {
+        prepareActivityThatShouldApplyUserMinAspectRatioOverride();
+
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
+             /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
+    }
+
+    @Test
     @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
     public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait()
             throws Exception {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java
index ef427bb..a8b2178 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlViewHostTests.java
@@ -16,7 +16,7 @@
 
 package com.android.server.wm;
 
-import static android.server.wm.CtsWindowInfoUtils.dumpWindowsOnScreen;
+import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState;
 import static android.server.wm.CtsWindowInfoUtils.waitForWindowFocus;
 import static android.server.wm.CtsWindowInfoUtils.waitForWindowVisible;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -129,37 +129,22 @@
             mScvh2.setView(mView2, lp2);
         });
 
-        boolean wasVisible = waitForWindowVisible(mView1);
-        if (!wasVisible) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows");
-        }
-        assertTrue("Failed to wait for view1", wasVisible);
-
-        wasVisible = waitForWindowVisible(mView2);
-        if (!wasVisible) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-not visible");
-        }
-        assertTrue("Failed to wait for view2", wasVisible);
+        assertAndDumpWindowState(TAG, "Failed to wait for view1", waitForWindowVisible(mView1));
+        assertAndDumpWindowState(TAG, "Failed to wait for view2", waitForWindowVisible(mView2));
 
         IWindow window = IWindow.Stub.asInterface(mSurfaceView.getWindowToken());
 
         WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(window,
                 mScvh1.getInputTransferToken(), true);
 
-        boolean gainedFocus = waitForWindowFocus(mView1, true);
-        if (!gainedFocus) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-view1 not focus");
-        }
-        assertTrue("Failed to gain focus for view1", gainedFocus);
+        assertAndDumpWindowState(TAG, "Failed to wait for view1 focus",
+                waitForWindowFocus(mView1, true));
 
         WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(window,
                 mScvh2.getInputTransferToken(), true);
 
-        gainedFocus = waitForWindowFocus(mView2, true);
-        if (!gainedFocus) {
-            dumpWindowsOnScreen(TAG, "requestFocusWithMultipleWindows-view2 not focus");
-        }
-        assertTrue("Failed to gain focus for view2", gainedFocus);
+        assertAndDumpWindowState(TAG, "Failed to wait for view2 focus",
+                waitForWindowFocus(mView2, true));
     }
 
     private static class TestWindowlessWindowManager extends WindowlessWindowManager {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 51f0404..8cd9ff3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -134,7 +134,6 @@
     private StaticMockitoSession mMockitoSession;
     private ActivityTaskManagerService mAtmService;
     private WindowManagerService mWmService;
-    private WindowState.PowerManagerWrapper mPowerManagerWrapper;
     private InputManagerService mImService;
     private InputChannel mInputChannel;
     private Runnable mOnBeforeServicesCreated;
@@ -360,7 +359,6 @@
     }
 
     private void setUpWindowManagerService() {
-        mPowerManagerWrapper = mock(WindowState.PowerManagerWrapper.class);
         TestWindowManagerPolicy wmPolicy = new TestWindowManagerPolicy();
         TestDisplayWindowSettingsProvider testDisplayWindowSettingsProvider =
                 new TestDisplayWindowSettingsProvider();
@@ -485,10 +483,6 @@
         return mAtmService;
     }
 
-    WindowState.PowerManagerWrapper getPowerManagerWrapper() {
-        return mPowerManagerWrapper;
-    }
-
     /** Creates a no-op wakelock object. */
     PowerManager.WakeLock createStubbedWakeLock(boolean needVerification) {
         if (needVerification) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java b/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java
index ac49839..6a15b05 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TrustedOverlayTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 
@@ -138,11 +139,8 @@
                     return false;
                 }, TIMEOUT_S, TimeUnit.SECONDS);
 
-        if (!foundTrusted[0]) {
-            CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, mName.getMethodName());
-        }
-
-        assertTrue("Failed to find window or was not marked trusted", foundTrusted[0]);
+        assertAndDumpWindowState(TAG, "Failed to find window or was not marked trusted",
+                foundTrusted[0]);
     }
 
     private void testTrustedOverlayChildHelper(boolean expectedTrustedChild)
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index f99b489..8bf4833 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -499,11 +499,16 @@
     public void testAddWindowWithSubWindowTypeByWindowContext() {
         spyOn(mWm.mWindowContextListenerController);
 
-        final WindowToken windowToken = createTestWindowToken(TYPE_INPUT_METHOD, mDefaultDisplay);
-        final Session session = getTestSession();
+        final WindowState parentWin = createWindow(null, TYPE_INPUT_METHOD, "ime");
+        final IBinder parentToken = parentWin.mToken.token;
+        parentWin.mAttrs.token = parentToken;
+        mWm.mWindowMap.put(parentToken, parentWin);
+        final Session session = parentWin.mSession;
+        session.onWindowAdded(parentWin);
         final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                 TYPE_APPLICATION_ATTACHED_DIALOG);
-        params.token = windowToken.token;
+        params.token = parentToken;
+        params.setTitle("attached-dialog");
         final IBinder windowContextToken = new Binder();
         params.setWindowContextToken(windowContextToken);
         doReturn(true).when(mWm.mWindowContextListenerController)
@@ -517,6 +522,12 @@
 
         verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(),
                 any(), any(), anyInt(), any(), anyBoolean());
+
+        assertTrue(parentWin.hasChild());
+        assertTrue(parentWin.isAttached());
+        session.binderDied();
+        assertFalse(parentWin.hasChild());
+        assertFalse(parentWin.isAttached());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 2007f68..75e252f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -52,7 +52,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -368,28 +367,26 @@
         firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
         secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
 
-        final WindowState.PowerManagerWrapper powerManagerWrapper =
-                mSystemServicesTestRule.getPowerManagerWrapper();
-        reset(powerManagerWrapper);
+        final var powerManager = mWm.mPowerManager;
+        clearInvocations(powerManager);
         firstWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
-        verify(powerManagerWrapper).wakeUp(anyLong(), anyInt(), anyString());
+        verify(powerManager).wakeUp(anyLong(), anyInt(), anyString());
 
-        reset(powerManagerWrapper);
+        clearInvocations(powerManager);
         secondWindow.prepareWindowToDisplayDuringRelayout(false /*wasVisible*/);
-        verify(powerManagerWrapper).wakeUp(anyLong(), anyInt(), anyString());
+        verify(powerManager).wakeUp(anyLong(), anyInt(), anyString());
     }
 
     private void testPrepareWindowToDisplayDuringRelayout(WindowState appWindow,
             boolean expectedWakeupCalled, boolean expectedCurrentLaunchCanTurnScreenOn) {
-        final WindowState.PowerManagerWrapper powerManagerWrapper =
-                mSystemServicesTestRule.getPowerManagerWrapper();
-        reset(powerManagerWrapper);
+        final var powerManager = mWm.mPowerManager;
+        clearInvocations(powerManager);
         appWindow.prepareWindowToDisplayDuringRelayout(false /* wasVisible */);
 
         if (expectedWakeupCalled) {
-            verify(powerManagerWrapper).wakeUp(anyLong(), anyInt(), anyString());
+            verify(powerManager).wakeUp(anyLong(), anyInt(), anyString());
         } else {
-            verify(powerManagerWrapper, never()).wakeUp(anyLong(), anyInt(), anyString());
+            verify(powerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
         }
         // If wakeup is expected to be called, the currentLaunchCanTurnScreenOn should be false
         // because the state will be consumed.
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index df4af11..a5f6190f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -294,7 +294,7 @@
      */
     static void suppressInsetsAnimation(InsetsControlTarget target) {
         spyOn(target);
-        Mockito.doNothing().when(target).notifyInsetsControlChanged();
+        Mockito.doNothing().when(target).notifyInsetsControlChanged(anyInt());
     }
 
     @After
@@ -637,14 +637,12 @@
     WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
             int ownerId, boolean ownerCanAddInternalSystemWindow, IWindow iwindow) {
         return createWindow(parent, type, token, name, ownerId, UserHandle.getUserId(ownerId),
-                ownerCanAddInternalSystemWindow, mWm, getTestSession(token), iwindow,
-                mSystemServicesTestRule.getPowerManagerWrapper());
+                ownerCanAddInternalSystemWindow, mWm, getTestSession(token), iwindow);
     }
 
     static WindowState createWindow(WindowState parent, int type, WindowToken token,
             String name, int ownerId, int userId, boolean ownerCanAddInternalSystemWindow,
-            WindowManagerService service, Session session, IWindow iWindow,
-            WindowState.PowerManagerWrapper powerManagerWrapper) {
+            WindowManagerService service, Session session, IWindow iWindow) {
         SystemServicesTestRule.checkHoldsLock(service.mGlobalLock);
 
         final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(type);
@@ -652,9 +650,7 @@
         attrs.packageName = "test";
 
         final WindowState w = new WindowState(service, session, iWindow, token, parent,
-                OP_NONE, attrs, VISIBLE, ownerId, userId,
-                ownerCanAddInternalSystemWindow,
-                powerManagerWrapper);
+                OP_NONE, attrs, VISIBLE, ownerId, userId, ownerCanAddInternalSystemWindow);
         // TODO: Probably better to make this call in the WindowState ctor to avoid errors with
         // adding it to the token...
         token.addWindow(w);
@@ -1738,17 +1734,14 @@
     static class TestStartingWindowOrganizer extends WindowOrganizerTests.StubOrganizer {
         private final ActivityTaskManagerService mAtm;
         private final WindowManagerService mWMService;
-        private final WindowState.PowerManagerWrapper mPowerManagerWrapper;
 
         private Runnable mRunnableWhenAddingSplashScreen;
         private final SparseArray<IBinder> mTaskAppMap = new SparseArray<>();
         private final HashMap<IBinder, WindowState> mAppWindowMap = new HashMap<>();
 
-        TestStartingWindowOrganizer(ActivityTaskManagerService service,
-                WindowState.PowerManagerWrapper powerManagerWrapper) {
+        TestStartingWindowOrganizer(ActivityTaskManagerService service) {
             mAtm = service;
             mWMService = mAtm.mWindowManager;
-            mPowerManagerWrapper = powerManagerWrapper;
             mAtm.mTaskOrganizerController.setDeferTaskOrgCallbacksConsumer(Runnable::run);
             mAtm.mTaskOrganizerController.registerTaskOrganizer(this);
         }
@@ -1767,8 +1760,7 @@
                 final WindowState window = WindowTestsBase.createWindow(null,
                         TYPE_APPLICATION_STARTING, activity,
                         "Starting window", 0 /* ownerId */, 0 /* userId*/,
-                        false /* internalWindows */, mWMService, createTestSession(mAtm), iWindow,
-                        mPowerManagerWrapper);
+                        false /* internalWindows */, mWMService, createTestSession(mAtm), iWindow);
                 activity.mStartingWindow = window;
                 mAppWindowMap.put(info.appToken, window);
                 mTaskAppMap.put(info.taskInfo.taskId, info.appToken);
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 4c978ad..2445f51 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -28,6 +28,7 @@
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.usage.ExternalStorageStats;
+import android.app.usage.Flags;
 import android.app.usage.IStorageStatsManager;
 import android.app.usage.StorageStats;
 import android.app.usage.UsageStatsManagerInternal;
@@ -434,6 +435,7 @@
         final long[] ceDataInodes = new long[packageNames.length];
         String[] codePaths = new String[0];
 
+        final PackageStats stats = new PackageStats(TAG);
         for (int i = 0; i < packageNames.length; i++) {
             try {
                 final ApplicationInfo appInfo = mPackage.getApplicationInfoAsUser(packageNames[i],
@@ -443,7 +445,11 @@
                 } else {
                     if (appInfo.getCodePath() != null) {
                         codePaths = ArrayUtils.appendElement(String.class, codePaths,
-                                appInfo.getCodePath());
+                            appInfo.getCodePath());
+                    }
+                    if (Flags.getAppBytesByDataTypeApi()) {
+                        computeAppStatsByDataTypes(
+                            stats, appInfo.sourceDir);
                     }
                 }
             } catch (NameNotFoundException e) {
@@ -451,7 +457,6 @@
             }
         }
 
-        final PackageStats stats = new PackageStats(TAG);
         try {
             mInstaller.getAppSize(volumeUuid, packageNames, userId, getDefaultFlags(),
                     appId, ceDataInodes, codePaths, stats);
@@ -587,6 +592,9 @@
         res.codeBytes = stats.codeSize + stats.externalCodeSize;
         res.dataBytes = stats.dataSize + stats.externalDataSize;
         res.cacheBytes = stats.cacheSize + stats.externalCacheSize;
+        res.apkBytes = stats.apkSize;
+        res.libBytes = stats.libSize;
+        res.dmBytes = stats.dmSize;
         res.externalCacheBytes = stats.externalCacheSize;
         return res;
     }
@@ -894,4 +902,61 @@
             mStorageStatsAugmenters.add(Pair.create(tag, storageStatsAugmenter));
         }
     }
+
+    private long getDirBytes(File dir) {
+        if (!dir.isDirectory()) {
+            return 0;
+        }
+
+        long size = 0;
+        try {
+            for (File file : dir.listFiles()) {
+                if (file.isFile()) {
+                    size += file.length();
+                    continue;
+                }
+                if (file.isDirectory()) {
+                    size += getDirBytes(file);
+                }
+            }
+        } catch (NullPointerException e) {
+            Slog.w(TAG, "Failed to list directory " + dir.getName());
+        }
+
+        return size;
+    }
+
+    private long getFileBytesInDir(File dir, String suffix) {
+        if (!dir.isDirectory()) {
+            return 0;
+        }
+
+        long size = 0;
+        try {
+            for (File file : dir.listFiles()) {
+                if (file.isFile() && file.getName().endsWith(suffix)) {
+                    size += file.length();
+                }
+            }
+        } catch (NullPointerException e) {
+             Slog.w(TAG, "Failed to list directory " + dir.getName());
+        }
+
+        return size;
+    }
+
+    private void computeAppStatsByDataTypes(
+        PackageStats stats, String sourceDirName) {
+
+        // Get apk, lib, dm file sizes.
+        File srcDir = new File(sourceDirName);
+        if (srcDir.isFile()) {
+            sourceDirName = srcDir.getParent();
+            srcDir = new File(sourceDirName);
+        }
+
+        stats.apkSize += getFileBytesInDir(srcDir, ".apk");
+        stats.dmSize += getFileBytesInDir(srcDir, ".dm");
+        stats.libSize += getDirBytes(new File(sourceDirName + "/lib/"));
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 72db7fe..08f719e 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1121,13 +1121,8 @@
 
             switch (event.mEventType) {
                 case Event.ACTIVITY_RESUMED:
-                    FrameworkStatsLog.write(
-                            FrameworkStatsLog.APP_USAGE_EVENT_OCCURRED,
-                            uid,
-                            event.mPackage,
-                            "",
-                            FrameworkStatsLog
-                                    .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__MOVE_TO_FOREGROUND);
+                    logAppUsageEventReportedAtomLocked(Event.ACTIVITY_RESUMED, uid, event.mPackage);
+
                     // check if this activity has already been resumed
                     if (mVisibleActivities.get(event.mInstanceId) != null) break;
                     final String usageSourcePackage = getUsageSourcePackage(event);
@@ -1172,13 +1167,8 @@
                                 usageSourcePackage2);
                         mVisibleActivities.put(event.mInstanceId, pausedData);
                     } else {
-                        FrameworkStatsLog.write(
-                                FrameworkStatsLog.APP_USAGE_EVENT_OCCURRED,
-                                uid,
-                                event.mPackage,
-                                "",
-                                FrameworkStatsLog
-                                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__MOVE_TO_BACKGROUND);
+                        logAppUsageEventReportedAtomLocked(Event.ACTIVITY_PAUSED, uid,
+                                event.mPackage);
                     }
 
                     pausedData.lastEvent = Event.ACTIVITY_PAUSED;
@@ -1203,13 +1193,8 @@
                     }
 
                     if (prevData.lastEvent != Event.ACTIVITY_PAUSED) {
-                        FrameworkStatsLog.write(
-                                FrameworkStatsLog.APP_USAGE_EVENT_OCCURRED,
-                                uid,
-                                event.mPackage,
-                                "",
-                                FrameworkStatsLog
-                                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__MOVE_TO_BACKGROUND);
+                        logAppUsageEventReportedAtomLocked(Event.ACTIVITY_PAUSED, uid,
+                                event.mPackage);
                     }
 
                     ArraySet<String> tokens;
@@ -1244,11 +1229,19 @@
                     }
                     break;
                 case Event.USER_INTERACTION:
-                    // Fall through
+                    logAppUsageEventReportedAtomLocked(Event.USER_INTERACTION, uid, event.mPackage);
+                    // Fall through.
                 case Event.APP_COMPONENT_USED:
                     convertToSystemTimeLocked(event);
                     mLastTimeComponentUsedGlobal.put(event.mPackage, event.mTimeStamp);
                     break;
+                case Event.SHORTCUT_INVOCATION:
+                case Event.CHOOSER_ACTION:
+                case Event.STANDBY_BUCKET_CHANGED:
+                case Event.FOREGROUND_SERVICE_START:
+                case Event.FOREGROUND_SERVICE_STOP:
+                    logAppUsageEventReportedAtomLocked(event.mEventType, uid, event.mPackage);
+                    break;
             }
 
             final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId);
@@ -1261,6 +1254,45 @@
         mIoHandler.obtainMessage(MSG_NOTIFY_USAGE_EVENT_LISTENER, userId, 0, event).sendToTarget();
     }
 
+    @GuardedBy("mLock")
+    private void logAppUsageEventReportedAtomLocked(int eventType, int uid, String packageName) {
+        FrameworkStatsLog.write(FrameworkStatsLog.APP_USAGE_EVENT_OCCURRED, uid, packageName,
+                "", getAppUsageEventOccurredAtomEventType(eventType));
+    }
+
+    /** Make sure align with the EventType defined in the AppUsageEventOccurred atom. */
+    private int getAppUsageEventOccurredAtomEventType(int eventType) {
+        switch (eventType) {
+            case Event.ACTIVITY_RESUMED:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__MOVE_TO_FOREGROUND;
+            case Event.ACTIVITY_PAUSED:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__MOVE_TO_BACKGROUND;
+            case Event.USER_INTERACTION:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__USER_INTERACTION;
+            case Event.SHORTCUT_INVOCATION:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__SHORTCUT_INVOCATION;
+            case Event.CHOOSER_ACTION:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__CHOOSER_ACTION;
+            case Event.STANDBY_BUCKET_CHANGED:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__STANDBY_BUCKET_CHANGED;
+            case Event.FOREGROUND_SERVICE_START:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__FOREGROUND_SERVICE_START;
+            case Event.FOREGROUND_SERVICE_STOP:
+                return FrameworkStatsLog
+                        .APP_USAGE_EVENT_OCCURRED__EVENT_TYPE__FOREGROUND_SERVICE_STOP;
+            default:
+                Slog.w(TAG, "Unsupported usage event logging: " + eventType);
+                return -1;
+        }
+    }
+
     private String getUsageSourcePackage(Event event) {
         switch(mUsageSource) {
             case USAGE_SOURCE_CURRENT_ACTIVITY:
@@ -2395,7 +2427,19 @@
                 return null;
             }
 
-            return queryEventsHelper(UserHandle.getCallingUserId(), query.getBeginTimeMillis(),
+            final int callingUserId = UserHandle.getCallingUserId();
+            int userId = query.getUserId();
+            if (userId == UserHandle.USER_NULL) {
+                // Convert userId to actual user Id if not specified in the query object.
+                userId = callingUserId;
+            }
+            if (userId != callingUserId) {
+                getContext().enforceCallingPermission(
+                        Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                        "No permission to query usage stats for user " + userId);
+            }
+
+            return queryEventsHelper(userId, query.getBeginTimeMillis(),
                     query.getEndTimeMillis(), callingPackage, query.getEventTypeFilter());
         }
 
diff --git a/services/usb/Android.bp b/services/usb/Android.bp
index 1dc5dcf..3a0a6ab 100644
--- a/services/usb/Android.bp
+++ b/services/usb/Android.bp
@@ -35,4 +35,7 @@
         "android.hardware.usb-V1.3-java",
         "android.hardware.usb-V3-java",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/services/voiceinteraction/Android.bp b/services/voiceinteraction/Android.bp
index de8d144..9e425b0 100644
--- a/services/voiceinteraction/Android.bp
+++ b/services/voiceinteraction/Android.bp
@@ -65,12 +65,14 @@
 
 java_library_static {
     name: "services.voiceinteraction",
-    defaults: ["platform_service_defaults"],
+    defaults: [
+        "platform_service_defaults",
+        "android.hardware.power-java_shared",
+    ],
     srcs: [":services.voiceinteraction-sources"],
     libs: [
         "services.core",
         "app-compat-annotations",
-        "android.hardware.power-V1-java",
         "android.hardware.power-V1.0-java",
     ],
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index 4720d27..f9fa9b7 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
@@ -106,96 +106,104 @@
             @Override
             public void onAttentionGained() {
                 Slog.v(TAG, "BinderCallback#onAttentionGained");
-                mEgressingData = true;
-                if (mAttentionListener == null) {
-                    return;
-                }
-                try {
-                    mAttentionListener.onAttentionGained();
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error delivering attention gained event.", e);
-                    try {
-                        callback.onVisualQueryDetectionServiceFailure(
-                                new VisualQueryDetectionServiceFailure(
-                                        ERROR_CODE_ILLEGAL_ATTENTION_STATE,
-                                        "Attention listener failed to switch to GAINED state."));
-                    } catch (RemoteException ex) {
-                        Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                synchronized (mLock) {
+                    mEgressingData = true;
+                    if (mAttentionListener == null) {
+                        return;
                     }
-                    return;
+                    try {
+                        mAttentionListener.onAttentionGained();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error delivering attention gained event.", e);
+                        try {
+                            callback.onVisualQueryDetectionServiceFailure(
+                                    new VisualQueryDetectionServiceFailure(
+                                            ERROR_CODE_ILLEGAL_ATTENTION_STATE,
+                                            "Attention listener fails to switch to GAINED state."));
+                        } catch (RemoteException ex) {
+                            Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                        }
+                    }
                 }
             }
 
             @Override
             public void onAttentionLost() {
                 Slog.v(TAG, "BinderCallback#onAttentionLost");
-                mEgressingData = false;
-                if (mAttentionListener == null) {
-                    return;
-                }
-                try {
-                    mAttentionListener.onAttentionLost();
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Error delivering attention lost event.", e);
-                    try {
-                        callback.onVisualQueryDetectionServiceFailure(
-                                new VisualQueryDetectionServiceFailure(
-                                        ERROR_CODE_ILLEGAL_ATTENTION_STATE,
-                                        "Attention listener failed to switch to LOST state."));
-                    } catch (RemoteException ex) {
-                        Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                synchronized (mLock) {
+                    mEgressingData = false;
+                    if (mAttentionListener == null) {
+                        return;
                     }
-                    return;
+                    try {
+                        mAttentionListener.onAttentionLost();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error delivering attention lost event.", e);
+                        try {
+                            callback.onVisualQueryDetectionServiceFailure(
+                                    new VisualQueryDetectionServiceFailure(
+                                            ERROR_CODE_ILLEGAL_ATTENTION_STATE,
+                                            "Attention listener fails to switch to LOST state."));
+                        } catch (RemoteException ex) {
+                            Slog.v(TAG, "Fail to call onVisualQueryDetectionServiceFailure");
+                        }
+                    }
                 }
             }
 
             @Override
             public void onQueryDetected(@NonNull String partialQuery) throws RemoteException {
-                Objects.requireNonNull(partialQuery);
                 Slog.v(TAG, "BinderCallback#onQueryDetected");
-                if (!mEgressingData) {
-                    Slog.v(TAG, "Query should not be egressed within the unattention state.");
-                    callback.onVisualQueryDetectionServiceFailure(
-                            new VisualQueryDetectionServiceFailure(
-                                    ERROR_CODE_ILLEGAL_STREAMING_STATE,
-                                    "Cannot stream queries without attention signals."));
-                    return;
+                synchronized (mLock) {
+                    Objects.requireNonNull(partialQuery);
+                    if (!mEgressingData) {
+                        Slog.v(TAG, "Query should not be egressed within the unattention state.");
+                        callback.onVisualQueryDetectionServiceFailure(
+                                new VisualQueryDetectionServiceFailure(
+                                        ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                                        "Cannot stream queries without attention signals."));
+                        return;
+                    }
+                    mQueryStreaming = true;
+                    callback.onQueryDetected(partialQuery);
+                    Slog.i(TAG, "Egressed from visual query detection process.");
                 }
-                mQueryStreaming = true;
-                callback.onQueryDetected(partialQuery);
-                Slog.i(TAG, "Egressed from visual query detection process.");
             }
 
             @Override
             public void onQueryFinished() throws RemoteException {
                 Slog.v(TAG, "BinderCallback#onQueryFinished");
-                if (!mQueryStreaming) {
-                    Slog.v(TAG, "Query streaming state signal FINISHED is block since there is"
-                            + " no active query being streamed.");
-                    callback.onVisualQueryDetectionServiceFailure(
-                            new VisualQueryDetectionServiceFailure(
-                                    ERROR_CODE_ILLEGAL_STREAMING_STATE,
-                                    "Cannot send FINISHED signal with no query streamed."));
-                    return;
+                synchronized (mLock) {
+                    if (!mQueryStreaming) {
+                        Slog.v(TAG, "Query streaming state signal FINISHED is block since there is"
+                                + " no active query being streamed.");
+                        callback.onVisualQueryDetectionServiceFailure(
+                                new VisualQueryDetectionServiceFailure(
+                                        ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                                        "Cannot send FINISHED signal with no query streamed."));
+                        return;
+                    }
+                    callback.onQueryFinished();
+                    mQueryStreaming = false;
                 }
-                callback.onQueryFinished();
-                mQueryStreaming = false;
             }
 
             @Override
             public void onQueryRejected() throws RemoteException {
                 Slog.v(TAG, "BinderCallback#onQueryRejected");
-                if (!mQueryStreaming) {
-                    Slog.v(TAG, "Query streaming state signal REJECTED is block since there is"
-                            + " no active query being streamed.");
-                    callback.onVisualQueryDetectionServiceFailure(
-                            new VisualQueryDetectionServiceFailure(
-                                    ERROR_CODE_ILLEGAL_STREAMING_STATE,
-                                    "Cannot send REJECTED signal with no query streamed."));
-                    return;
+                synchronized (mLock) {
+                    if (!mQueryStreaming) {
+                        Slog.v(TAG, "Query streaming state signal REJECTED is block since there is"
+                                + " no active query being streamed.");
+                        callback.onVisualQueryDetectionServiceFailure(
+                                new VisualQueryDetectionServiceFailure(
+                                        ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                                        "Cannot send REJECTED signal with no query streamed."));
+                        return;
+                    }
+                    callback.onQueryRejected();
+                    mQueryStreaming = false;
                 }
-                callback.onQueryRejected();
-                mQueryStreaming = false;
             }
         };
         return mRemoteDetectionService.run(
diff --git a/telecomm/java/android/telecom/AuthenticatorService.java b/telecomm/java/android/telecom/AuthenticatorService.java
deleted file mode 100644
index 1e43c71..0000000
--- a/telecomm/java/android/telecom/AuthenticatorService.java
+++ /dev/null
@@ -1,101 +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.telecom;
-import android.accounts.AbstractAccountAuthenticator;
-import android.accounts.Account;
-import android.accounts.AccountAuthenticatorResponse;
-import android.accounts.NetworkErrorException;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.IBinder;
-
-/**
- * A generic stub account authenticator service often used for sync adapters that do not directly
- * involve accounts.
- *
- * @hide
- */
-public class AuthenticatorService extends Service {
-    private static Authenticator mAuthenticator;
-
-    @Override
-    public void onCreate() {
-        mAuthenticator = new Authenticator(this);
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mAuthenticator.getIBinder();
-    }
-
-    /**
-     * Stub account authenticator. All methods either return null or throw an exception.
-     */
-    public class Authenticator extends AbstractAccountAuthenticator {
-        public Authenticator(Context context) {
-            super(context);
-        }
-
-        @Override
-        public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse,
-                                     String s) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse,
-                                 String s, String s2, String[] strings, Bundle bundle)
-                throws NetworkErrorException {
-            return null;
-        }
-
-        @Override
-        public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
-                                         Account account, Bundle bundle)
-                throws NetworkErrorException {
-            return null;
-        }
-
-        @Override
-        public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse,
-                                   Account account, String s, Bundle bundle)
-                throws NetworkErrorException {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public String getAuthTokenLabel(String s) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
-                                        Account account, String s, Bundle bundle)
-                throws NetworkErrorException {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse,
-                                  Account account, String[] strings)
-                throws NetworkErrorException {
-            throw new UnsupportedOperationException();
-        }
-    }
-}
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index def52a5..874c10c 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -210,100 +210,6 @@
             "android.telecom.extra.SILENT_RINGING_REQUESTED";
 
     /**
-     * Call event sent from a {@link Call} via {@link #sendCallEvent(String, Bundle)} to inform
-     * Telecom that the user has requested that the current {@link Call} should be handed over
-     * to another {@link ConnectionService}.
-     * <p>
-     * The caller must specify the {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE} to indicate to
-     * Telecom which {@link PhoneAccountHandle} the {@link Call} should be handed over to.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_REQUEST_HANDOVER =
-            "android.telecom.event.REQUEST_HANDOVER";
-
-    /**
-     * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event.  Specifies the
-     * {@link PhoneAccountHandle} to which a call should be handed over to.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE =
-            "android.telecom.extra.HANDOVER_PHONE_ACCOUNT_HANDLE";
-
-    /**
-     * Integer extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event.  Specifies the
-     * video state of the call when it is handed over to the new {@link PhoneAccount}.
-     * <p>
-     * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY},
-     * {@link VideoProfile#STATE_BIDIRECTIONAL}, {@link VideoProfile#STATE_RX_ENABLED}, and
-     * {@link VideoProfile#STATE_TX_ENABLED}.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EXTRA_HANDOVER_VIDEO_STATE =
-            "android.telecom.extra.HANDOVER_VIDEO_STATE";
-
-    /**
-     * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event.  Used by the
-     * {@link InCallService} initiating a handover to provide a {@link Bundle} with extra
-     * information to the handover {@link ConnectionService} specified by
-     * {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE}.
-     * <p>
-     * This {@link Bundle} is not interpreted by Telecom, but passed as-is to the
-     * {@link ConnectionService} via the request extras when
-     * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}
-     * is called to initate the handover.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EXTRA_HANDOVER_EXTRAS = "android.telecom.extra.HANDOVER_EXTRAS";
-
-    /**
-     * Call event sent from Telecom to the handover {@link ConnectionService} via
-     * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover
-     * to the {@link ConnectionService} has completed successfully.
-     * <p>
-     * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_COMPLETE =
-            "android.telecom.event.HANDOVER_COMPLETE";
-
-    /**
-     * Call event sent from Telecom to the handover destination {@link ConnectionService} via
-     * {@link Connection#onCallEvent(String, Bundle)} to inform the handover destination that the
-     * source connection has disconnected.  The {@link Bundle} parameter for the call event will be
-     * {@code null}.
-     * <p>
-     * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_SOURCE_DISCONNECTED =
-            "android.telecom.event.HANDOVER_SOURCE_DISCONNECTED";
-
-    /**
-     * Call event sent from Telecom to the handover {@link ConnectionService} via
-     * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover
-     * to the {@link ConnectionService} has failed.
-     * <p>
-     * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_FAILED =
-            "android.telecom.event.HANDOVER_FAILED";
-
-    /**
      * Event reported from the Telecom stack to report an in-call diagnostic message which the
      * dialer app may opt to display to the user.  A diagnostic message is used to communicate
      * scenarios the device has detected which may impact the quality of the ongoing call.
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 4a541da..a2105b0 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -961,28 +961,6 @@
             "android.telecom.event.CALL_REMOTELY_UNHELD";
 
     /**
-     * Connection event used to inform an {@link InCallService} which initiated a call handover via
-     * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has
-     * successfully completed.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_COMPLETE =
-            "android.telecom.event.HANDOVER_COMPLETE";
-
-    /**
-     * Connection event used to inform an {@link InCallService} which initiated a call handover via
-     * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has failed
-     * to complete.
-     * @hide
-     * @deprecated Use {@link Call#handoverTo(PhoneAccountHandle, int, Bundle)} and its associated
-     * APIs instead.
-     */
-    public static final String EVENT_HANDOVER_FAILED =
-            "android.telecom.event.HANDOVER_FAILED";
-
-    /**
      * String Connection extra key used to store SIP invite fields for an incoming call for IMS call
      */
     public static final String EXTRA_SIP_INVITE = "android.telecom.extra.SIP_INVITE";
@@ -3362,15 +3340,6 @@
     public void onDisconnect() {}
 
     /**
-     * Notifies this Connection of a request to disconnect a participant of the conference managed
-     * by the connection.
-     *
-     * @param endpoint the {@link Uri} of the participant to disconnect.
-     * @hide
-     */
-    public void onDisconnectConferenceParticipant(Uri endpoint) {}
-
-    /**
      * Notifies this Connection of a request to separate from its parent conference.
      */
     public void onSeparate() {}
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index b167f1b..d05eb5c 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1349,6 +1349,24 @@
     }
 
     /**
+     * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
+     * calls. The returned list includes those accounts which have been explicitly enabled by
+     * the user or other users visible to the user.
+     *
+     * @see #EXTRA_PHONE_ACCOUNT_HANDLE
+     * @return A list of {@code PhoneAccountHandle} objects.
+     *
+     * @throws IllegalStateException if telecom service is null.
+     */
+    @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_WORK_PROFILE_API_SPLIT)
+    @RequiresPermission(allOf = {android.Manifest.permission.READ_PHONE_STATE,
+            android.Manifest.permission.INTERACT_ACROSS_PROFILES})
+    public @NonNull List<PhoneAccountHandle> getCallCapablePhoneAccountsAcrossProfiles() {
+        return getCallCapablePhoneAccountsAcrossProfiles(false);
+    }
+
+
+    /**
      * Returns a list of {@link PhoneAccountHandle}s for all self-managed
      * {@link ConnectionService}s owned by the calling {@link UserHandle}.
      * <p>
@@ -1423,7 +1441,7 @@
         if (service != null) {
             try {
                 return service.getCallCapablePhoneAccounts(includeDisabledAccounts,
-                        mContext.getOpPackageName(), mContext.getAttributionTag()).getList();
+                        mContext.getOpPackageName(), mContext.getAttributionTag(), false).getList();
             } catch (RemoteException e) {
                 Log.e(TAG, "Error calling ITelecomService#getCallCapablePhoneAccounts("
                         + includeDisabledAccounts + ")", e);
@@ -1433,6 +1451,37 @@
     }
 
     /**
+     * Returns a list of {@link PhoneAccountHandle}s visible to current user including those which
+     * have not been enabled by the user.
+     *
+     * @param includeDisabledAccounts When {@code true}, disabled phone accounts will be included,
+     *                                when {@code false}, only enabled phone accounts will be
+     *                                included.
+     * @return A list of {@code PhoneAccountHandle} objects.
+     *
+     * @throws IllegalStateException if telecom service is null.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_WORK_PROFILE_API_SPLIT)
+    @RequiresPermission(allOf = {android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            android.Manifest.permission.INTERACT_ACROSS_PROFILES})
+    public @NonNull List<PhoneAccountHandle> getCallCapablePhoneAccountsAcrossProfiles(
+            boolean includeDisabledAccounts) {
+        ITelecomService service = getTelecomService();
+        if (service == null) {
+            throw new IllegalStateException("telecom service is null.");
+        }
+
+        try {
+            return service.getCallCapablePhoneAccounts(includeDisabledAccounts,
+                    mContext.getOpPackageName(), mContext.getAttributionTag(), true).getList();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
      *  Returns a list of all {@link PhoneAccount}s registered for the calling package.
      *
      * @deprecated Use {@link #getSelfManagedPhoneAccounts()} instead to get only self-managed
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index 53154e6..f1bfd22 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -61,7 +61,8 @@
      * @see TelecomServiceImpl#getCallCapablePhoneAccounts
      */
     ParceledListSlice<PhoneAccountHandle> getCallCapablePhoneAccounts(
-            boolean includeDisabledAccounts, String callingPackage, String callingFeatureId);
+            boolean includeDisabledAccounts, String callingPackage,
+            String callingFeatureId, boolean acrossProfiles);
 
     /**
      * @see TelecomServiceImpl#getSelfManagedPhoneAccounts
diff --git a/telephony/java/android/telephony/AnomalyReporter.java b/telephony/java/android/telephony/AnomalyReporter.java
index db38f88..575ec27 100644
--- a/telephony/java/android/telephony/AnomalyReporter.java
+++ b/telephony/java/android/telephony/AnomalyReporter.java
@@ -187,14 +187,15 @@
         }
 
         for (ResolveInfo r : packages) {
-            if (r.activityInfo == null
-                    || pm.checkPermission(
+            if (r.activityInfo == null) {
+                Rlog.w(TAG, "Found package without activity");
+                continue;
+            } else if (pm.checkPermission(
                             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
                             r.activityInfo.packageName)
-                    != PackageManager.PERMISSION_GRANTED) {
-                Rlog.w(TAG,
-                        "Found package without proper permissions or no activity"
-                                + r.activityInfo.packageName);
+                      != PackageManager.PERMISSION_GRANTED) {
+                Rlog.w(TAG, "Found package without proper permissions"
+                                    + r.activityInfo.packageName);
                 continue;
             }
             Rlog.d(TAG, "Found a valid package " + r.activityInfo.packageName);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 7cb2cc3..c7b84a3 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -525,6 +525,12 @@
     public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
 
     /**
+     * Used in the Preferred Network Types menu to determine if the 3G option is displayed.
+     */
+    @FlaggedApi(Flags.FLAG_HIDE_PREFER_3G_ITEM)
+    public static final String KEY_PREFER_3G_VISIBILITY_BOOL = "prefer_3g_visibility_bool";
+
+    /**
      * Used in Cellular Network Settings for preferred network type to show 4G only mode.
      * @hide
      */
@@ -3715,19 +3721,19 @@
      * This configuration allows the system UI to display different 5G icons for different 5G
      * scenarios.
      *
-     * There are five 5G scenarios:
-     * 1. connected_mmwave: device currently connected to 5G cell as the secondary cell and using
-     *    millimeter wave.
-     * 2. connected: device currently connected to 5G cell as the secondary cell but not using
-     *    millimeter wave.
-     * 3. not_restricted_rrc_idle: device camped on a network that has 5G capability(not necessary
-     *    to connect a 5G cell as a secondary cell) and the use of 5G is not restricted and RRC
-     *    currently in IDLE state.
-     * 4. not_restricted_rrc_con: device camped on a network that has 5G capability(not necessary
-     *    to connect a 5G cell as a secondary cell) and the use of 5G is not restricted and RRC
-     *    currently in CONNECTED state.
-     * 5. restricted: device camped on a network that has 5G capability(not necessary to connect a
-     *    5G cell as a secondary cell) but the use of 5G is restricted.
+     * There are six 5G scenarios for icon configuration:
+     * 1. connected_mmwave: device currently connected to 5G cell as the primary or secondary cell
+     *    and considered NR advanced.
+     * 2. connected: device currently connected to 5G cell as the primary or secondary cell but not
+     *    considered NR advanced.
+     * 3. connected_rrc_idle: device currently connected to 5G cell as the primary or secondary cell
+     *    and RRC currently in IDLE state.
+     * 4. not_restricted_rrc_idle: device camped on a network that has 5G capability and the use of
+     *    5G is not restricted and RRC currently in IDLE state.
+     * 5. not_restricted_rrc_con: device camped on a network that has 5G capability and the use of
+     *    5G is not restricted and RRC currently in CONNECTED state.
+     * 6. restricted: device camped on a network that has 5G capability but the use of 5G is
+     *    restricted.
      *
      * The configured string contains multiple key-value pairs separated by comma. For each pair,
      * the key and value are separated by a colon. The key corresponds to a 5G status above and
@@ -3748,21 +3754,21 @@
      * This configuration allows the system UI to determine how long to continue to display 5G icons
      * when the device switches between different 5G scenarios.
      *
-     * There are seven 5G scenarios:
-     * 1. connected_mmwave: device currently connected to 5G cell as the secondary cell and using
-     *    millimeter wave.
-     * 2. connected: device currently connected to 5G cell as the secondary cell but not using
-     *    millimeter wave.
-     * 3. not_restricted_rrc_idle: device camped on a network that has 5G capability (not necessary
-     *    to connect a 5G cell as a secondary cell) and the use of 5G is not restricted and RRC
-     *    currently in IDLE state.
-     * 4. not_restricted_rrc_con: device camped on a network that has 5G capability (not necessary
-     *    to connect a 5G cell as a secondary cell) and the use of 5G is not restricted and RRC
-     *    currently in CONNECTED state.
-     * 5. restricted: device camped on a network that has 5G capability (not necessary to connect a
-     *    5G cell as a secondary cell) but the use of 5G is restricted.
-     * 6. legacy: device is not camped on a network that has 5G capability
-     * 7. any: any of the above scenarios
+     * There are eight 5G scenarios:
+     * 1. connected_mmwave: device currently connected to 5G cell as the primary or secondary cell
+     *    and considered NR advanced.
+     * 2. connected: device currently connected to 5G cell as the primary or secondary cell but not
+     *    considered NR advanced.
+     * 3. connected_rrc_idle: device currently connected to 5G cell as the primary or secondary cell
+     *    and RRC currently in IDLE state.
+     * 4. not_restricted_rrc_idle: device camped on a network that has 5G capability and the use of
+     *    5G is not restricted and RRC currently in IDLE state.
+     * 5. not_restricted_rrc_con: device camped on a network that has 5G capability and the use of
+     *    5G is not restricted and RRC currently in CONNECTED state.
+     * 6. restricted: device camped on a network that has 5G capability but the use of 5G is
+     *    restricted.
+     * 7. legacy: device is not camped on a network that has 5G capability
+     * 8. any: any of the above scenarios
      *
      * The configured string contains various timer rules separated by a semicolon.
      * Each rule will have three items: prior 5G scenario, current 5G scenario, and grace period
@@ -3770,8 +3776,8 @@
      * 5G scenario, the system UI will continue to show the icon for the prior 5G scenario (defined
      * in {@link #KEY_5G_ICON_CONFIGURATION_STRING}) for the amount of time specified by the grace
      * period. If the prior 5G scenario is reestablished, the timer will reset and start again if
-     * the UE changes 5G scenarios again. Defined states (5G scenarios #1-5) take precedence over
-     * 'any' (5G scenario #6), and unspecified transitions have a default grace period of 0.
+     * the UE changes 5G scenarios again. Defined states (5G scenarios #1-7) take precedence over
+     * 'any' (5G scenario #8), and unspecified transitions have a default grace period of 0.
      * The order of rules in the configuration determines the priority (the first applicable timer
      * rule will be used).
      *
@@ -3794,21 +3800,21 @@
      * This configuration extends {@link #KEY_5G_ICON_DISPLAY_GRACE_PERIOD_STRING} to allow the
      * system UI to continue displaying 5G icons after the initial timer expires.
      *
-     * There are seven 5G scenarios:
-     * 1. connected_mmwave: device currently connected to 5G cell as the secondary cell and using
-     *    millimeter wave.
-     * 2. connected: device currently connected to 5G cell as the secondary cell but not using
-     *    millimeter wave.
-     * 3. not_restricted_rrc_idle: device camped on a network that has 5G capability (not necessary
-     *    to connect a 5G cell as a secondary cell) and the use of 5G is not restricted and RRC
-     *    currently in IDLE state.
-     * 4. not_restricted_rrc_con: device camped on a network that has 5G capability (not necessary
-     *    to connect a 5G cell as a secondary cell) and the use of 5G is not restricted and RRC
-     *    currently in CONNECTED state.
-     * 5. restricted: device camped on a network that has 5G capability (not necessary to connect a
-     *    5G cell as a secondary cell) but the use of 5G is restricted.
-     * 6. legacy: device is not camped on a network that has 5G capability
-     * 7. any: any of the above scenarios
+     * There are eight 5G scenarios:
+     * 1. connected_mmwave: device currently connected to 5G cell as the primary or secondary cell
+     *    and considered NR advanced.
+     * 2. connected: device currently connected to 5G cell as the primary or secondary cell but not
+     *    considered NR advanced.
+     * 3. connected_rrc_idle: device currently connected to 5G cell as the primary or secondary cell
+     *    and RRC currently in IDLE state.
+     * 4. not_restricted_rrc_idle: device camped on a network that has 5G capability and the use of
+     *    5G is not restricted and RRC currently in IDLE state.
+     * 5. not_restricted_rrc_con: device camped on a network that has 5G capability and the use of
+     *    5G is not restricted and RRC currently in CONNECTED state.
+     * 6. restricted: device camped on a network that has 5G capability but the use of 5G is
+     *    restricted.
+     * 7. legacy: device is not camped on a network that has 5G capability
+     * 8. any: any of the above scenarios
      *
      * The configured string contains various timer rules separated by a semicolon.
      * Each rule will have three items: primary 5G scenario, secondary 5G scenario, and
@@ -3818,7 +3824,7 @@
      * period. If the primary 5G scenario is reestablished, the timers will reset and the system UI
      * will continue to display the icon for the primary 5G scenario without interruption. If the
      * secondary 5G scenario is lost, the timer will reset and the icon will reflect the true state.
-     * Defined states (5G scenarios #1-5) take precedence over 'any' (5G scenario #6), and
+     * Defined states (5G scenarios #1-7) take precedence over 'any' (5G scenario #8), and
      * unspecified transitions have a default grace period of 0. The order of rules in the
      * configuration determines the priority (the first applicable timer rule will be used).
      *
@@ -8885,18 +8891,18 @@
                 KEY_PREFIX + "epdg_static_address_roaming_string";
 
         /**
-         * Controls if the multiple SA proposals allowed for IKE session to include
-         * all the 3GPP TS 33.210 and RFC 8221 supported cipher suites in multiple
-         * IKE SA proposals as per RFC 7296.
+         * Enables the use of multiple IKE SA proposals, encompassing both carrier-preferred
+         * ciphers and all supported ciphers from 3GPP TS 33.210 and RFC 8221,
+         * as defined in RFC 7296.
          */
         @FlaggedApi(Flags.FLAG_ENABLE_MULTIPLE_SA_PROPOSALS)
         public static final String KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL =
                 KEY_PREFIX + "supports_ike_session_multiple_sa_proposals_bool";
 
         /**
-         * Controls if the multiple SA proposals allowed for Child session to include
-         * all the 3GPP TS 33.210 and RFC 8221 supported cipher suites in multiple
-         * Child SA proposals as per RFC 7296.
+         * Enables the use of multiple Child SA proposals, encompassing both carrier-preferred
+         * ciphers and all supported ciphers from 3GPP TS 33.210 and RFC 8221,
+         * as defined in RFC 7296.
          */
         @FlaggedApi(Flags.FLAG_ENABLE_MULTIPLE_SA_PROPOSALS)
         public static final String KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL =
@@ -9577,14 +9583,20 @@
             "satellite_attach_supported_bool";
 
     /**
-     * The carrier-enabled satellite connection hysteresis time in seconds to determine whether to
-     * recommend Dialer to prompt users to use satellite emergency messaging.
+     * The carrier-enabled satellite connection hysteresis time in seconds for which the device
+     * continues in satellite mode after it loses the connection with the satellite network.
      * <p>
-     * A timer is started when there is an ongoing emergency call, and the IMS is not registered,
-     * and cellular service is not available, and the device was connected to a satellite network
-     * within this time in the past. When the timer expires, Telephony will send the event
+     * If the device is in satellite mode, the following actions will be taken by the device:
+     * <ul>
+     * <li>System UI will continue showing the satellite icon.</li>
+     * <li>When there is an ongoing emergency call, and the IMS is not registered, and cellular
+     * service is not available, and the device is in satellite mode, a timer with a duration
+     * defined by the overlay config
+     * {@code config_emergency_call_wait_for_connection_timeout_millis} will be started. When the
+     * timer expires, Telephony will send the event
      * {@link TelephonyManager#EVENT_DISPLAY_EMERGENCY_MESSAGE} to Dialer, which will then prompt
-     * users to switch to using satellite emergency messaging.
+     * users to switch to using satellite emergency messaging.</li>
+     * </ul>
      * <p>
      * The default value is 300 seconds.
      */
@@ -10038,6 +10050,49 @@
     public static final String KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_BUNDLE =
             "auto_data_switch_rat_signal_score_string_bundle";
 
+    // TODO(b/316183370): replace @code with @link in javadoc after feature is released
+    /**
+     * An array of cellular services supported by a subscription.
+     *
+     * <p>Permissible values include:
+     * <ul>
+     *   <li>{@code SubscriptionManager#SERVICE_CAPABILITY_VOICE} for voice services</li>
+     *   <li>{@code SubscriptionManager#SERVICE_CAPABILITY_SMS} for SMS services</li>
+     *   <li>{@code SubscriptionManager#SERVICE_CAPABILITY_DATA} for data services</li>
+     * </ul>
+     *
+     * <p>Carrier-specific factors may influence how these services are supported. Therefore,
+     * modifying this carrier configuration might not always enable the specified services. These
+     * capability bitmasks should be considered as indicators of a carrier's preferred services
+     * to enhance user experience, rather than as absolute platform guarantees.
+     *
+     * <p>Device-level service capabilities, defined by
+     * {@code TelephonyManager#isDeviceVoiceCapable} and
+     * {@code TelephonyManager#isDeviceSmsCapable}, take precedence over these subscription-level
+     * settings. For instance, a device where {@code TelephonyManager#isDeviceVoiceCapable} returns
+     * false may not be able to make voice calls, even if subscribed to a service marked as
+     * voice-capable.
+     *
+     * <p>To determine a subscription's cellular service capabilities, use
+     * {@code SubscriptionInfo#getServiceCapabilities()}. To track changes in services, register
+     * a {@link SubscriptionManager.OnSubscriptionsChangedListener} and invoke the
+     * same method in its callback.
+     *
+     * <p>Emergency service availability may not depend on the cellular service capabilities.
+     * For example, emergency calls might be possible on a subscription even if it lacks
+     * {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE}.
+     *
+     * <p>If unset, the default value is “[1, 2, 3]” (supports all cellular services).
+     *
+     * @see TelephonyManager#isDeviceVoiceCapable
+     * @see TelephonyManager#isDeviceSmsCapable
+     * @see SubscriptionInfo#getServiceCapabilities()
+     * @see SubscriptionManager.OnSubscriptionsChangedListener
+     */
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public static final String KEY_CELLULAR_SERVICE_CAPABILITIES_INT_ARRAY =
+            "cellular_service_capabilities_int_array";
+
     /** The default value for every variable. */
     private static final PersistableBundle sDefaults;
 
@@ -10138,6 +10193,7 @@
         sDefaults.putBoolean(KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL, false);
         sDefaults.putBoolean(KEY_OPERATOR_SELECTION_EXPAND_BOOL, true);
         sDefaults.putBoolean(KEY_PREFER_2G_BOOL, false);
+        sDefaults.putBoolean(KEY_PREFER_3G_VISIBILITY_BOOL, true);
         sDefaults.putBoolean(KEY_4G_ONLY_BOOL, false);
         sDefaults.putBoolean(KEY_SHOW_APN_SETTING_CDMA_BOOL, false);
         sDefaults.putBoolean(KEY_SHOW_CDMA_CHOICES_BOOL, false);
@@ -10552,7 +10608,7 @@
         sDefaults.putBoolean(KEY_USE_CALL_WAITING_USSD_BOOL, false);
         sDefaults.putInt(KEY_CALL_WAITING_SERVICE_CLASS_INT, 1 /* SERVICE_CLASS_VOICE */);
         sDefaults.putString(KEY_5G_ICON_CONFIGURATION_STRING,
-                "connected_mmwave:5G,connected:5G,not_restricted_rrc_idle:5G,"
+                "connected_mmwave:5G,connected:5G,connected_rrc_idle:5G,not_restricted_rrc_idle:5G,"
                         + "not_restricted_rrc_con:5G");
         sDefaults.putString(KEY_5G_ICON_DISPLAY_GRACE_PERIOD_STRING, "");
         sDefaults.putString(KEY_5G_ICON_DISPLAY_SECONDARY_GRACE_PERIOD_STRING, "");
@@ -10824,6 +10880,7 @@
                 new boolean[] {false, false, true, false, false});
         sDefaults.putStringArray(KEY_CARRIER_SERVICE_NAME_STRING_ARRAY, new String[0]);
         sDefaults.putStringArray(KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY, new String[0]);
+        sDefaults.putIntArray(KEY_CELLULAR_SERVICE_CAPABILITIES_INT_ARRAY, new int[]{1, 2, 3});
     }
 
     /**
diff --git a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java
new file mode 100644
index 0000000..61d7ead
--- /dev/null
+++ b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java
@@ -0,0 +1,214 @@
+/*
+ * 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.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A single occurrence capturing a notable change to previously reported
+ * cryptography algorithms for a given network and network event.
+ *
+ * @hide
+ */
+public final class SecurityAlgorithmUpdate implements Parcelable {
+    private static final String TAG = "SecurityAlgorithmUpdate";
+
+    private @ConnectionEvent int mConnectionEvent;
+    private @SecurityAlgorithm int mEncryption;
+    private @SecurityAlgorithm int mIntegrity;
+    private boolean mIsUnprotectedEmergency;
+
+    public SecurityAlgorithmUpdate(@ConnectionEvent int connectionEvent,
+            @SecurityAlgorithm int encryption, @SecurityAlgorithm int integrity,
+            boolean isUnprotectedEmergency) {
+        mConnectionEvent = connectionEvent;
+        mEncryption = encryption;
+        mIntegrity = integrity;
+        mIsUnprotectedEmergency = isUnprotectedEmergency;
+    }
+
+    private SecurityAlgorithmUpdate(Parcel in) {
+        readFromParcel(in);
+    }
+
+    public @ConnectionEvent int getConnectionEvent() {
+        return mConnectionEvent;
+    }
+
+    public @SecurityAlgorithm int getEncryption() {
+        return mEncryption;
+    }
+
+    public @SecurityAlgorithm int getIntegrity() {
+        return mIntegrity;
+    }
+
+    public boolean isUnprotectedEmergency() {
+        return mIsUnprotectedEmergency;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mConnectionEvent);
+        out.writeInt(mEncryption);
+        out.writeInt(mIntegrity);
+        out.writeBoolean(mIsUnprotectedEmergency);
+    }
+
+    private void readFromParcel(@NonNull Parcel in) {
+        mConnectionEvent = in.readInt();
+        mEncryption = in.readInt();
+        mIntegrity = in.readInt();
+        mIsUnprotectedEmergency = in.readBoolean();
+    }
+
+    public static final Parcelable.Creator<SecurityAlgorithmUpdate> CREATOR =
+            new Parcelable.Creator<SecurityAlgorithmUpdate>() {
+                public SecurityAlgorithmUpdate createFromParcel(Parcel in) {
+                    return new SecurityAlgorithmUpdate(in);
+                }
+
+                public SecurityAlgorithmUpdate[] newArray(int size) {
+                    return new SecurityAlgorithmUpdate[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return TAG + ":{ mConnectionEvent = " + mConnectionEvent + " mEncryption = " + mEncryption
+                + " mIntegrity = " + mIntegrity + " mIsUnprotectedEmergency = "
+                + mIsUnprotectedEmergency;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof SecurityAlgorithmUpdate)) return false;
+        SecurityAlgorithmUpdate that = (SecurityAlgorithmUpdate) o;
+        return mConnectionEvent == that.mConnectionEvent
+                && mEncryption == that.mEncryption
+                && mIntegrity == that.mIntegrity
+                && mIsUnprotectedEmergency == that.mIsUnprotectedEmergency;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mConnectionEvent, mEncryption, mIntegrity, mIsUnprotectedEmergency);
+    }
+
+    public static final int CONNECTION_EVENT_CS_SIGNALLING_GSM = 0;
+    public static final int CONNECTION_EVENT_PS_SIGNALLING_GPRS = 1;
+    public static final int CONNECTION_EVENT_CS_SIGNALLING_3G = 2;
+    public static final int CONNECTION_EVENT_PS_SIGNALLING_3G = 3;
+    public static final int CONNECTION_EVENT_NAS_SIGNALLING_LTE = 4;
+    public static final int CONNECTION_EVENT_AS_SIGNALLING_LTE = 5;
+    public static final int CONNECTION_EVENT_VOLTE_SIP = 6;
+    public static final int CONNECTION_EVENT_VOLTE_RTP = 7;
+    public static final int CONNECTION_EVENT_NAS_SIGNALLING_5G = 8;
+    public static final int CONNECTION_EVENT_AS_SIGNALLING_5G = 9;
+    public static final int CONNECTION_EVENT_VONR_SIP = 10;
+    public static final int CONNECTION_EVENT_VONR_RTP = 11;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"CONNECTION_EVENT_"}, value = {CONNECTION_EVENT_CS_SIGNALLING_GSM,
+            CONNECTION_EVENT_PS_SIGNALLING_GPRS, CONNECTION_EVENT_CS_SIGNALLING_3G,
+            CONNECTION_EVENT_PS_SIGNALLING_3G, CONNECTION_EVENT_NAS_SIGNALLING_LTE,
+            CONNECTION_EVENT_AS_SIGNALLING_LTE, CONNECTION_EVENT_VOLTE_SIP,
+            CONNECTION_EVENT_VOLTE_RTP, CONNECTION_EVENT_NAS_SIGNALLING_5G,
+            CONNECTION_EVENT_AS_SIGNALLING_5G, CONNECTION_EVENT_VONR_SIP,
+            CONNECTION_EVENT_VONR_RTP})
+    public @interface ConnectionEvent {
+    }
+
+    public static final int SECURITY_ALGORITHM_A50 = 0;
+    public static final int SECURITY_ALGORITHM_A51 = 1;
+    public static final int SECURITY_ALGORITHM_A52 = 2;
+    public static final int SECURITY_ALGORITHM_A53 = 3;
+    public static final int SECURITY_ALGORITHM_A54 = 4;
+    public static final int SECURITY_ALGORITHM_GEA0 = 14;
+    public static final int SECURITY_ALGORITHM_GEA1 = 15;
+    public static final int SECURITY_ALGORITHM_GEA2 = 16;
+    public static final int SECURITY_ALGORITHM_GEA3 = 17;
+    public static final int SECURITY_ALGORITHM_GEA4 = 18;
+    public static final int SECURITY_ALGORITHM_GEA5 = 19;
+    public static final int SECURITY_ALGORITHM_UEA0 = 29;
+    public static final int SECURITY_ALGORITHM_UEA1 = 30;
+    public static final int SECURITY_ALGORITHM_UEA2 = 31;
+    public static final int SECURITY_ALGORITHM_EEA0 = 41;
+    public static final int SECURITY_ALGORITHM_EEA1 = 42;
+    public static final int SECURITY_ALGORITHM_EEA2 = 43;
+    public static final int SECURITY_ALGORITHM_EEA3 = 44;
+    public static final int SECURITY_ALGORITHM_NEA0 = 55;
+    public static final int SECURITY_ALGORITHM_NEA1 = 56;
+    public static final int SECURITY_ALGORITHM_NEA2 = 57;
+    public static final int SECURITY_ALGORITHM_NEA3 = 58;
+    public static final int SECURITY_ALGORITHM_SIP_NULL = 68;
+    public static final int SECURITY_ALGORITHM_AES_GCM = 69;
+    public static final int SECURITY_ALGORITHM_AES_GMAC = 70;
+    public static final int SECURITY_ALGORITHM_AES_CBC = 71;
+    public static final int SECURITY_ALGORITHM_DES_EDE3_CBC = 72;
+    public static final int SECURITY_ALGORITHM_AES_EDE3_CBC = 73;
+    public static final int SECURITY_ALGORITHM_HMAC_SHA1_96 = 74;
+    public static final int SECURITY_ALGORITHM_HMAC_SHA1_96_NULL = 75;
+    public static final int SECURITY_ALGORITHM_HMAC_MD5_96 = 76;
+    public static final int SECURITY_ALGORITHM_HMAC_MD5_96_NULL = 77;
+    public static final int SECURITY_ALGORITHM_SRTP_AES_COUNTER = 87;
+    public static final int SECURITY_ALGORITHM_SRTP_AES_F8 = 88;
+    public static final int SECURITY_ALGORITHM_SRTP_HMAC_SHA1 = 89;
+    public static final int SECURITY_ALGORITHM_ENCR_AES_GCM_16 = 99;
+    public static final int SECURITY_ALGORITHM_ENCR_AES_CBC = 100;
+    public static final int SECURITY_ALGORITHM_AUTH_HMAC_SHA2_256_128 = 101;
+    public static final int SECURITY_ALGORITHM_UNKNOWN = 113;
+    public static final int SECURITY_ALGORITHM_OTHER = 114;
+    public static final int SECURITY_ALGORITHM_ORYX = 124;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"CONNECTION_EVENT_"}, value = {SECURITY_ALGORITHM_A50, SECURITY_ALGORITHM_A51,
+            SECURITY_ALGORITHM_A52, SECURITY_ALGORITHM_A53,
+            SECURITY_ALGORITHM_A54, SECURITY_ALGORITHM_GEA0, SECURITY_ALGORITHM_GEA1,
+            SECURITY_ALGORITHM_GEA2, SECURITY_ALGORITHM_GEA3, SECURITY_ALGORITHM_GEA4,
+            SECURITY_ALGORITHM_GEA5, SECURITY_ALGORITHM_UEA0, SECURITY_ALGORITHM_UEA1,
+            SECURITY_ALGORITHM_UEA2, SECURITY_ALGORITHM_EEA0, SECURITY_ALGORITHM_EEA1,
+            SECURITY_ALGORITHM_EEA2, SECURITY_ALGORITHM_EEA3, SECURITY_ALGORITHM_NEA0,
+            SECURITY_ALGORITHM_NEA1, SECURITY_ALGORITHM_NEA2, SECURITY_ALGORITHM_NEA3,
+            SECURITY_ALGORITHM_SIP_NULL, SECURITY_ALGORITHM_AES_GCM,
+            SECURITY_ALGORITHM_AES_GMAC, SECURITY_ALGORITHM_AES_CBC,
+            SECURITY_ALGORITHM_DES_EDE3_CBC, SECURITY_ALGORITHM_AES_EDE3_CBC,
+            SECURITY_ALGORITHM_HMAC_SHA1_96, SECURITY_ALGORITHM_HMAC_SHA1_96_NULL,
+            SECURITY_ALGORITHM_HMAC_MD5_96, SECURITY_ALGORITHM_HMAC_MD5_96_NULL,
+            SECURITY_ALGORITHM_SRTP_AES_COUNTER, SECURITY_ALGORITHM_SRTP_AES_F8,
+            SECURITY_ALGORITHM_SRTP_HMAC_SHA1, SECURITY_ALGORITHM_ENCR_AES_GCM_16,
+            SECURITY_ALGORITHM_ENCR_AES_CBC, SECURITY_ALGORITHM_AUTH_HMAC_SHA2_256_128,
+            SECURITY_ALGORITHM_UNKNOWN, SECURITY_ALGORITHM_OTHER, SECURITY_ALGORITHM_ORYX})
+    public @interface SecurityAlgorithm {
+    }
+
+}
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index 60cc9c7..df349f8 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -3556,4 +3556,29 @@
         }
         return smscUri;
     }
+
+    /**
+     * Gets the message size of a WAP from the cache.
+     *
+     * @param locationUrl the location to use as a key for looking up the size in the cache.
+     * The locationUrl may or may not have the transactionId appended to the url.
+     *
+     * @return long representing the message size
+     * @throws java.util.NoSuchElementException if the WAP push doesn't exist in the cache
+     * @throws IllegalArgumentException if the locationUrl is empty
+     *
+     * @hide
+     */
+    public long getWapMessageSize(@NonNull String locationUrl) {
+        try {
+            ISms iSms = getISmsService();
+            if (iSms != null) {
+                return iSms.getWapMessageSize(locationUrl);
+            } else {
+                throw new RuntimeException("Could not acquire ISms service.");
+            }
+        } catch (RemoteException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index 6ebf3be..a188581 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -54,6 +54,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * A Parcelable class for Subscription Information.
@@ -262,6 +263,11 @@
     private final boolean mIsOnlyNonTerrestrialNetwork;
 
     /**
+     * The service capabilities (in the form of bitmask combination) the subscription supports.
+     */
+    private final int mServiceCapabilities;
+
+    /**
      * @hide
      *
      * @deprecated Use {@link SubscriptionInfo.Builder}.
@@ -386,6 +392,7 @@
         this.mPortIndex = portIndex;
         this.mUsageSetting = usageSetting;
         this.mIsOnlyNonTerrestrialNetwork = false;
+        this.mServiceCapabilities = 0;
     }
 
     /**
@@ -425,6 +432,7 @@
         this.mPortIndex = builder.mPortIndex;
         this.mUsageSetting = builder.mUsageSetting;
         this.mIsOnlyNonTerrestrialNetwork = builder.mIsOnlyNonTerrestrialNetwork;
+        this.mServiceCapabilities = builder.mServiceCapabilities;
     }
 
     /**
@@ -882,6 +890,44 @@
         return mIsOnlyNonTerrestrialNetwork;
     }
 
+    // TODO(b/316183370): replace @code with @link in javadoc after feature is released
+    /**
+     * Retrieves the service capabilities for the current subscription.
+     *
+     * <p>These capabilities are hint to system components and applications, allowing them to
+     * enhance user experience. For instance, a Dialer application can inform the user that the
+     * current subscription is incapable of making voice calls if the voice service is not
+     * available.
+     *
+     * <p>Correct usage of these service capabilities must also consider the device's overall
+     * service capabilities. For example, even if the subscription supports voice calls, a voice
+     * call might not be feasible on a device that only supports data services. To determine the
+     * device's capabilities for voice and SMS services, refer to
+     * {@code TelephonyManager#isDeviceVoiceCapable()} and
+     * {@code TelephonyManager#isDeviceSmsCapable()}.
+     *
+     * <p>Emergency service availability may not directly correlate with the subscription or
+     * device's general service capabilities. In some cases, emergency calls might be possible
+     * even if the subscription or device does not typically support voice services.
+     *
+     * @return A set of integer representing the subscription's service capabilities,
+     * defined by {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE},
+     * {@code SubscriptionManager#SERVICE_CAPABILITY_SMS}
+     * and {@code SubscriptionManager#SERVICE_CAPABILITY_DATA}.
+     *
+     * @see TelephonyManager#isDeviceVoiceCapable()
+     * @see TelephonyManager#isDeviceSmsCapable()
+     * @see CarrierConfigManager#KEY_CELLULAR_SERVICE_CAPABILITIES_INT_ARRAY
+     * @see SubscriptionManager#SERVICE_CAPABILITY_VOICE
+     * @see SubscriptionManager#SERVICE_CAPABILITY_SMS
+     * @see SubscriptionManager#SERVICE_CAPABILITY_DATA
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public @SubscriptionManager.ServiceCapability Set<Integer> getServiceCapabilities() {
+        return SubscriptionManager.getServiceCapabilitiesSet(mServiceCapabilities);
+    }
+
     @NonNull
     public static final Parcelable.Creator<SubscriptionInfo> CREATOR =
             new Parcelable.Creator<SubscriptionInfo>() {
@@ -919,6 +965,8 @@
                     .setUiccApplicationsEnabled(source.readBoolean())
                     .setUsageSetting(source.readInt())
                     .setOnlyNonTerrestrialNetwork(source.readBoolean())
+                    .setServiceCapabilities(
+                            SubscriptionManager.getServiceCapabilitiesSet(source.readInt()))
                     .build();
         }
 
@@ -961,6 +1009,7 @@
         dest.writeBoolean(mAreUiccApplicationsEnabled);
         dest.writeInt(mUsageSetting);
         dest.writeBoolean(mIsOnlyNonTerrestrialNetwork);
+        dest.writeInt(mServiceCapabilities);
     }
 
     @Override
@@ -1024,6 +1073,8 @@
                 + " areUiccApplicationsEnabled=" + mAreUiccApplicationsEnabled
                 + " usageSetting=" + SubscriptionManager.usageSettingToString(mUsageSetting)
                 + " isOnlyNonTerrestrialNetwork=" + mIsOnlyNonTerrestrialNetwork
+                + " serviceCapabilities=" + SubscriptionManager.getServiceCapabilitiesSet(
+                mServiceCapabilities).toString()
                 + "]";
     }
 
@@ -1049,7 +1100,8 @@
                 that.mNativeAccessRules) && Arrays.equals(mCarrierConfigAccessRules,
                 that.mCarrierConfigAccessRules) && Objects.equals(mGroupUuid, that.mGroupUuid)
                 && mCountryIso.equals(that.mCountryIso) && mGroupOwner.equals(that.mGroupOwner)
-                && mIsOnlyNonTerrestrialNetwork == that.mIsOnlyNonTerrestrialNetwork;
+                && mIsOnlyNonTerrestrialNetwork == that.mIsOnlyNonTerrestrialNetwork
+                && mServiceCapabilities == that.mServiceCapabilities;
     }
 
     @Override
@@ -1058,7 +1110,7 @@
                 mDisplayNameSource, mIconTint, mNumber, mDataRoaming, mMcc, mMnc, mIsEmbedded,
                 mCardString, mIsOpportunistic, mGroupUuid, mCountryIso, mCarrierId, mProfileClass,
                 mType, mGroupOwner, mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting, mCardId,
-                mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork);
+                mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork, mServiceCapabilities);
         result = 31 * result + Arrays.hashCode(mEhplmns);
         result = 31 * result + Arrays.hashCode(mHplmns);
         result = 31 * result + Arrays.hashCode(mNativeAccessRules);
@@ -1263,6 +1315,11 @@
         private boolean mIsOnlyNonTerrestrialNetwork = false;
 
         /**
+         * Service capabilities bitmasks the subscription supports.
+         */
+        private int mServiceCapabilities = 0;
+
+        /**
          * Default constructor.
          */
         public Builder() {
@@ -1305,6 +1362,7 @@
             mPortIndex = info.mPortIndex;
             mUsageSetting = info.mUsageSetting;
             mIsOnlyNonTerrestrialNetwork = info.mIsOnlyNonTerrestrialNetwork;
+            mServiceCapabilities = info.mServiceCapabilities;
         }
 
         /**
@@ -1703,6 +1761,32 @@
         }
 
         /**
+         * Set the service capabilities that the subscription supports.
+         *
+         * @param capabilities Bitmask combination of SubscriptionManager
+         *                     .SERVICE_CAPABILITY_XXX.
+         * @return The builder.
+         *
+         * @throws IllegalArgumentException when any capability is not supported.
+         */
+        @NonNull
+        @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+        public Builder setServiceCapabilities(
+                @NonNull @SubscriptionManager.ServiceCapability Set<Integer> capabilities) {
+            int combinedCapabilities = 0;
+            for (int capability : capabilities) {
+                if (capability < SubscriptionManager.SERVICE_CAPABILITY_VOICE
+                        || capability > SubscriptionManager.SERVICE_CAPABILITY_MAX) {
+                    throw new IllegalArgumentException(
+                            "Invalid service capability value: " + capability);
+                }
+                combinedCapabilities |= SubscriptionManager.serviceCapabilityToBitmask(capability);
+            }
+            mServiceCapabilities = combinedCapabilities;
+            return this;
+        }
+
+        /**
          * Build the {@link SubscriptionInfo}.
          *
          * @return The {@link SubscriptionInfo} instance.
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 326b6f5..6c8663a 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -88,10 +88,12 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -1138,6 +1140,14 @@
      */
     public static final String IS_NTN = SimInfo.COLUMN_IS_NTN;
 
+    /**
+     * TelephonyProvider column name to identify service capabilities.
+     * Disabled by default.
+     * <P>Type: INTEGER (int)</P>
+     * @hide
+     */
+    public static final String SERVICE_CAPABILITIES = SimInfo.COLUMN_SERVICE_CAPABILITIES;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"USAGE_SETTING_"},
@@ -1347,6 +1357,86 @@
             })
     public @interface PhoneNumberSource {}
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"SERVICE_CAPABILITY"},
+            value = {
+                    SERVICE_CAPABILITY_VOICE,
+                    SERVICE_CAPABILITY_SMS,
+                    SERVICE_CAPABILITY_DATA,
+            })
+    public @interface ServiceCapability {
+    }
+
+    /**
+     * Represents a value indicating the voice calling capabilities of a subscription.
+     *
+     * <p>This value identifies whether the subscription supports various voice calling services.
+     * These services can include circuit-switched (CS) calling, packet-switched (PS) IMS (IP
+     * Multimedia Subsystem) calling, and over-the-top (OTT) calling options.
+     *
+     * <p>Note: The availability of emergency calling services is not solely dependent on this
+     * voice capability. Emergency services may be accessible even if the subscription lacks
+     * standard voice capabilities. However, the device's ability to support emergency calls
+     * can be influenced by its inherent voice capabilities, as determined by
+     * {@link TelephonyManager#isDeviceVoiceCapable()}.
+     *
+     * @see TelephonyManager#isDeviceVoiceCapable()
+     */
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public static final int SERVICE_CAPABILITY_VOICE = 1;
+
+    /**
+     * Represents a value indicating the SMS capabilities of a subscription.
+     *
+     * <p>This value identifies whether the subscription supports various sms services.
+     * These services can include circuit-switched (CS) SMS, packet-switched (PS) IMS (IP
+     * Multimedia Subsystem) SMS, and over-the-top (OTT) SMS options.
+     *
+     * <p>Note: The availability of emergency SMS services is not solely dependent on this
+     * sms capability. Emergency services may be accessible even if the subscription lacks
+     * standard sms capabilities. However, the device's ability to support emergency sms
+     * can be influenced by its inherent sms capabilities, as determined by
+     * {@link TelephonyManager#isDeviceSmsCapable()}.
+     *
+     * @see TelephonyManager#isDeviceSmsCapable()
+     */
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public static final int SERVICE_CAPABILITY_SMS = 2;
+
+    /**
+     * Represents a value indicating the data calling capabilities of a subscription.
+     */
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public static final int SERVICE_CAPABILITY_DATA = 3;
+
+    /**
+     * Maximum value of service capabilities supported so far.
+     * @hide
+     */
+    public static final int SERVICE_CAPABILITY_MAX = SERVICE_CAPABILITY_DATA;
+
+    /**
+     * Bitmask for {@code SERVICE_CAPABILITY_VOICE}.
+     * @hide
+     */
+    public static final int SERVICE_CAPABILITY_VOICE_BITMASK =
+            serviceCapabilityToBitmask(SERVICE_CAPABILITY_VOICE);
+
+    /**
+     * Bitmask for {@code SERVICE_CAPABILITY_SMS}.
+     * @hide
+     */
+    public static final int SERVICE_CAPABILITY_SMS_BITMASK =
+            serviceCapabilityToBitmask(SERVICE_CAPABILITY_SMS);
+
+    /**
+     * Bitmask for {@code SERVICE_CAPABILITY_DATA}.
+     * @hide
+     */
+    public static final int SERVICE_CAPABILITY_DATA_BITMASK =
+            serviceCapabilityToBitmask(SERVICE_CAPABILITY_DATA);
+
     private final Context mContext;
 
     /**
@@ -4484,4 +4574,38 @@
         }
         return new ArrayList<>();
     }
+
+    /**
+     * @return the bitmasks combination of all service capabilities.
+     * @hide
+     */
+    public static int getAllServiceCapabilityBitmasks() {
+        return SERVICE_CAPABILITY_VOICE_BITMASK | SERVICE_CAPABILITY_SMS_BITMASK
+                | SERVICE_CAPABILITY_DATA_BITMASK;
+    }
+
+    /**
+     * @return The set of service capability from a bitmask combined one.
+     * @hide
+     */
+    @NonNull
+    @ServiceCapability
+    public static Set<Integer> getServiceCapabilitiesSet(int combinedServiceCapabilities) {
+        Set<Integer> capabilities = new HashSet<>();
+        for (int i = SERVICE_CAPABILITY_VOICE; i <= SERVICE_CAPABILITY_MAX; i++) {
+            final int capabilityBitmask = serviceCapabilityToBitmask(i);
+            if ((combinedServiceCapabilities & capabilityBitmask) == capabilityBitmask) {
+                capabilities.add(i);
+            }
+        }
+        return Collections.unmodifiableSet(capabilities);
+    }
+
+    /**
+     * @return The service capability bitmask from a {@link ServiceCapability} value.
+     * @hide
+     */
+    public static int serviceCapabilityToBitmask(@ServiceCapability int capability) {
+        return 1 << (capability - 1);
+    }
 }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 55a3da5..9e292be 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -1207,7 +1207,8 @@
      * The dialer app receives this event via
      * {@link Call.Callback#onConnectionEvent(Call, String, Bundle)}.
      * <p>
-     * The {@link Bundle} parameter is expected to include the following extras:
+     * The {@link Bundle} parameter is guaranteed to include the following extras if the below
+     * conditions are met:
      * <ul>
      *     <li>{@link #EXTRA_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE} - the recommending handover
      *         type.</li>
@@ -2114,6 +2115,20 @@
     public static final String ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE =
             "com.android.omadm.service.CONFIGURATION_UPDATE";
 
+    /**
+     * Activity action: Show setting to reset mobile networks.
+     *
+     * <p>On devices with a settings activity to reset mobile networks, the activity should be
+     * launched without additional permissions.
+     *
+     * <p>On some devices, this settings activity may not exist. Callers should ensure that this
+     * case is appropriately handled.
+     */
+    @FlaggedApi(Flags.FLAG_RESET_MOBILE_NETWORK_SETTINGS)
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_RESET_MOBILE_NETWORK_SETTINGS =
+            "android.telephony.action.RESET_MOBILE_NETWORK_SETTINGS";
+
     //
     //
     // Device Info
@@ -6682,8 +6697,10 @@
      * PackageManager.FEATURE_TELEPHONY system feature, which is available
      * on any device with a telephony radio, even if the device is
      * data-only.
+     * @deprecated Replaced by {@link #isDeviceVoiceCapable()}
      */
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
+    @Deprecated
     public boolean isVoiceCapable() {
         if (mContext == null) return true;
         return mContext.getResources().getBoolean(
@@ -6691,6 +6708,30 @@
     }
 
     /**
+     * @return true if the current device is "voice capable".
+     * <p>
+     * "Voice capable" means that this device supports circuit-switched or IMS packet switched
+     * (i.e. voice) phone calls over the telephony network, and is allowed to display the in-call
+     * UI while a cellular voice call is active. This will be false on "data only" devices which
+     * can't make voice calls and don't support any in-call UI.
+     * <p>
+     * Note: the meaning of this flag is subtly different from the PackageManager
+     * .FEATURE_TELEPHONY system feature, which is available on any device with a telephony
+     * radio, even if the device is data-only.
+     * <p>
+     * To check if a subscription is "voice capable", call method
+     * {@link SubscriptionInfo#getServiceCapabilities()} and compare  the result with
+     * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_VOICE}.
+     *
+     * @see SubscriptionInfo#getServiceCapabilities()
+     */
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public boolean isDeviceVoiceCapable() {
+        return isVoiceCapable();
+    }
+
+    /**
      * @return true if the current device supports sms service.
      * <p>
      * If true, this means that the device supports both sending and
@@ -6698,6 +6739,7 @@
      * <p>
      * Note: Voicemail waiting sms, cell broadcasting sms, and MMS are
      *       disabled when device doesn't support sms.
+     * @deprecated Replaced by {@link #isDeviceSmsCapable()}
      */
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
     public boolean isSmsCapable() {
@@ -6707,6 +6749,27 @@
     }
 
     /**
+     * @return true if the current device supports SMS service.
+     * <p>
+     * If true, this means that the device supports both sending and
+     * receiving SMS via the telephony network.
+     * <p>
+     * Note: Voicemail waiting SMS, cell broadcasting SMS, and MMS are
+     *       disabled when device doesn't support SMS.
+     * <p>
+     * To check if a subscription is "SMS capable", call method
+     * {@link SubscriptionInfo#getServiceCapabilities()} and compare result with
+     * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_SMS}.
+     *
+     * @see SubscriptionInfo#getServiceCapabilities()
+     */
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
+    @FlaggedApi(Flags.FLAG_DATA_ONLY_CELLULAR_SERVICE)
+    public boolean isDeviceSmsCapable() {
+        return isSmsCapable();
+    }
+
+    /**
      * Requests all available cell information from all radios on the device including the
      * camped/registered, serving, and neighboring cells.
      *
diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java
index 2172d7d..71bb329 100644
--- a/telephony/java/android/telephony/ims/ImsMmTelManager.java
+++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java
@@ -19,6 +19,7 @@
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresFeature;
@@ -45,6 +46,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.IIntegerConsumer;
 import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -447,6 +449,114 @@
     }
 
     /**
+     * Registers a {@link RegistrationCallback} with the system, which will provide IMS emergency
+     * registration updates for the subscription specified in
+     * {@link ImsManager#getImsMmTelManager(int)}. Use
+     * {@link SubscriptionManager.OnSubscriptionsChangedListener} to listen to Subscription changed
+     * events and call {@link #unregisterImsRegistrationCallback(RegistrationCallback)} to clean up.
+     *
+     * When the callback is registered, it will initiate the callback c to be called with the
+     * current emergency registration state.
+     * Emergency registration callback is available when there is valid SIM card.
+     * <p>This API requires one of the following:
+     * <ul>
+     *     <li>The caller holds the READ_PRECISE_PHONE_STATE permission.</li>
+     *     <li>If the caller is the device or profile owner, the caller holds the
+     *     {@link Manifest.permission#READ_PRECISE_PHONE_STATE} permission.</li>
+     *     <li>The caller has carrier privileges (see
+     *     {@link android.telephony.TelephonyManager#hasCarrierPrivileges}) on any
+     *     active subscription.</li>
+     * </ul>
+     * <p>The profile owner is an app that owns a managed profile on the device; for more details
+     * see <a href="https://developer.android.com/work/managed-profiles">Work profiles</a>.
+     * Access by profile owners is deprecated and will be removed in a future release.
+     *
+     * @param executor The executor the callback events should be run on.
+     * @param c The {@link RegistrationCallback} to be added.
+     * @see #unregisterImsEmergencyRegistrationCallback
+     * @throws ImsException if the subscription associated with this callback is valid, but
+     * the {@link ImsService} associated with the subscription is not available. This can happen if
+     * the service crashed, for example. See {@link ImsException#getCode()} for a more detailed
+     * reason.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+    public void registerImsEmergencyRegistrationCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull RegistrationManager.RegistrationCallback c) throws ImsException {
+        if (c == null) {
+            throw new IllegalArgumentException("Must include a non-null RegistrationCallback.");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException("Must include a non-null Executor.");
+        }
+        c.setExecutor(executor);
+
+        ITelephony iTelephony = getITelephony();
+        if (iTelephony == null) {
+            throw new ImsException("Could not find Telephony Service.",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+
+        try {
+            iTelephony.registerImsEmergencyRegistrationCallback(mSubId, c.getBinder());
+        } catch (ServiceSpecificException e) {
+            throw new ImsException(e.getMessage(), e.errorCode);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }  catch (IllegalStateException e) {
+            throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    /**
+     * Removes an existing {@link RegistrationCallback} for Emergency IMS registration.
+     *
+     * When the subscription associated with this callback is removed (SIM removed, ESIM swap,
+     * etc...), this callback will automatically be removed. If this method is called for an
+     * inactive subscription, it will result in a no-op.
+     * <p>This API requires one of the following:
+     * <ul>
+     *     <li>The caller holds the READ_PRECISE_PHONE_STATE permission.</li>
+     *     <li>If the caller is the device or profile owner, the caller holds the
+     *     {@link Manifest.permission#READ_PRECISE_PHONE_STATE} permission.</li>
+     *     <li>The caller has carrier privileges (see
+     *     {@link android.telephony.TelephonyManager#hasCarrierPrivileges}) on any
+     *     active subscription.</li>
+     * </ul>
+     * <p>The profile owner is an app that owns a managed profile on the device; for more details
+     * see <a href="https://developer.android.com/work/managed-profiles">Work profiles</a>.
+     * Access by profile owners is deprecated and will be removed in a future release.
+     *
+     * @param c The {@link RegistrationCallback} to be removed.
+     * @see android.telephony.SubscriptionManager.OnSubscriptionsChangedListener
+     * @see #registerImsEmergencyRegistrationCallback(Executor,
+     *                                 RegistrationManager.RegistrationCallback)
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+    public void unregisterImsEmergencyRegistrationCallback(
+            @NonNull RegistrationManager.RegistrationCallback c) {
+        if (c == null) {
+            throw new IllegalArgumentException("Must include a non-null RegistrationCallback.");
+        }
+
+        ITelephony iTelephony = getITelephony();
+        if (iTelephony == null) {
+            Log.w("ImsMmTelManager", "Could not find Telephony Service.");
+            return;
+        }
+
+        try {
+            iTelephony.unregisterImsEmergencyRegistrationCallback(mSubId, c.getBinder());
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
      * {@inheritDoc}
      * @hide
      */
diff --git a/telephony/java/android/telephony/ims/ImsRegistrationAttributes.java b/telephony/java/android/telephony/ims/ImsRegistrationAttributes.java
index 50c438c..f548dba 100644
--- a/telephony/java/android/telephony/ims/ImsRegistrationAttributes.java
+++ b/telephony/java/android/telephony/ims/ImsRegistrationAttributes.java
@@ -16,6 +16,7 @@
 
 package android.telephony.ims;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,6 +27,8 @@
 import android.telephony.ims.stub.ImsRegistrationImplBase;
 import android.util.ArraySet;
 
+import com.android.internal.telephony.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -46,12 +49,36 @@
      *
      */
     public static final int ATTR_EPDG_OVER_CELL_INTERNET = 1 << 0;
+    /**
+     * Attribute to specify if ims registration is of type normal or emergency.
+     * <p>
+     *     For emergency registration bit will be set.
+     *     For normal registration bit will not be set.
+     *     This flag is only applicable when listening to emergency IMS registration state updates
+     *     via the ImsMmTelManager#registerImsEmergencyRegistrationCallback API
+     * </p>
+     */
+    @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+    public static final int ATTR_REGISTRATION_TYPE_EMERGENCY = 1 << 1;
+    /**
+     * Attribute to specify if virtual registration is required.
+     * <p>
+     *     If emergency registration is not required for making emergency call, in such cases
+     *     bit will be set and callback will represent virtual registration status update.
+     *     This flag is only applicable when listening to emergency IMS registration state updates
+     *     via the ImsMmTelManager#registerImsEmergencyRegistrationCallback API
+     * </p>
+     */
+    @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+    public static final int ATTR_VIRTUAL_FOR_ANONYMOUS_EMERGENCY_CALL = 1 << 2;
 
     /** @hide */
     // Defines the underlying radio technology type that we have registered for IMS over.
     @IntDef(prefix = "ATTR_",
             value = {
                     ATTR_EPDG_OVER_CELL_INTERNET,
+                    ATTR_REGISTRATION_TYPE_EMERGENCY,
+                    ATTR_VIRTUAL_FOR_ANONYMOUS_EMERGENCY_CALL,
             },
             flag = true)
     @Retention(RetentionPolicy.SOURCE)
@@ -67,6 +94,8 @@
         private Set<String> mFeatureTags = Collections.emptySet();
         private @Nullable SipDetails mSipDetails;
 
+        private @ImsAttributeFlag int mAttributeFlags;
+
         /**
          * Build a new instance of {@link ImsRegistrationAttributes}.
          *
@@ -74,6 +103,9 @@
          */
         public Builder(@ImsRegistrationImplBase.ImsRegistrationTech int registrationTech) {
             mRegistrationTech = registrationTech;
+            if (registrationTech == ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM) {
+                mAttributeFlags |= ATTR_EPDG_OVER_CELL_INTERNET;
+            }
         }
 
         /**
@@ -110,25 +142,32 @@
         }
 
         /**
+         * Set the attribute flag ATTR_REGISTRATION_TYPE_EMERGENCY.
+         */
+        @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+        public @NonNull Builder setFlagRegistrationTypeEmergency() {
+            mAttributeFlags |= ATTR_REGISTRATION_TYPE_EMERGENCY;
+            return this;
+        }
+
+        /**
+         * Set the attribute flag ATTR_VIRTUAL_FOR_ANONYMOUS_EMERGENCY_CALL.
+         */
+        @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+        public @NonNull Builder setFlagVirtualRegistrationForEmergencyCall() {
+            mAttributeFlags |= ATTR_VIRTUAL_FOR_ANONYMOUS_EMERGENCY_CALL;
+            return this;
+        }
+
+        /**
          * @return A new instance created from this builder.
          */
         public @NonNull ImsRegistrationAttributes build() {
             return new ImsRegistrationAttributes(mRegistrationTech,
                     RegistrationManager.getAccessType(mRegistrationTech),
-                    getAttributeFlags(mRegistrationTech),
+                    mAttributeFlags,
                     mFeatureTags, mSipDetails);
         }
-
-        /**
-         * @return attribute flags from the registration technology.
-         */
-        private static int getAttributeFlags(int imsRadioTech) {
-            int attributes = 0;
-            if (imsRadioTech == ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM) {
-                attributes |= ATTR_EPDG_OVER_CELL_INTERNET;
-            }
-            return attributes;
-        }
     }
 
     private final int mRegistrationTech;
diff --git a/telephony/java/android/telephony/ims/aidl/IImsRegistration.aidl b/telephony/java/android/telephony/ims/aidl/IImsRegistration.aidl
index 219c9c8..2a6b8f2 100644
--- a/telephony/java/android/telephony/ims/aidl/IImsRegistration.aidl
+++ b/telephony/java/android/telephony/ims/aidl/IImsRegistration.aidl
@@ -28,6 +28,8 @@
    int getRegistrationTechnology();
    oneway void addRegistrationCallback(IImsRegistrationCallback c);
    oneway void removeRegistrationCallback(IImsRegistrationCallback c);
+   oneway void addEmergencyRegistrationCallback(IImsRegistrationCallback c);
+   oneway void removeEmergencyRegistrationCallback(IImsRegistrationCallback c);
    oneway void triggerFullNetworkRegistration(int sipCode, String sipReason);
    oneway void triggerUpdateSipDelegateRegistration();
    oneway void triggerSipDelegateDeregistration();
diff --git a/telephony/java/android/telephony/ims/stub/ImsRegistrationImplBase.java b/telephony/java/android/telephony/ims/stub/ImsRegistrationImplBase.java
index ff378ba..99c26b0 100644
--- a/telephony/java/android/telephony/ims/stub/ImsRegistrationImplBase.java
+++ b/telephony/java/android/telephony/ims/stub/ImsRegistrationImplBase.java
@@ -16,6 +16,7 @@
 
 package android.telephony.ims.stub;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -31,6 +32,7 @@
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.util.Log;
 
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.util.RemoteCallbackListExt;
 import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.internal.util.ArrayUtils;
@@ -56,7 +58,6 @@
 public class ImsRegistrationImplBase {
 
     private static final String LOG_TAG = "ImsRegistrationImplBase";
-
     /**
      * @hide
      */
@@ -233,6 +234,31 @@
         }
 
         @Override
+        public void addEmergencyRegistrationCallback(IImsRegistrationCallback c)
+                throws RemoteException {
+            AtomicReference<RemoteException> exceptionRef = new AtomicReference<>();
+            executeMethodAsync(() -> {
+                try {
+                    ImsRegistrationImplBase.this.addEmergencyRegistrationCallback(c);
+                } catch (RemoteException e) {
+                    exceptionRef.set(e);
+                }
+            }, "addEmergencyRegistrationCallback");
+
+            if (exceptionRef.get() != null) {
+                throw exceptionRef.get();
+            }
+        }
+
+        @Override
+        public void removeEmergencyRegistrationCallback(IImsRegistrationCallback c)
+                throws RemoteException {
+            executeMethodAsync(() ->
+                    ImsRegistrationImplBase.this.removeEmergencyRegistrationCallback(c),
+                    "removeEmergencyRegistrationCallback");
+        }
+
+        @Override
         public void removeRegistrationCallback(IImsRegistrationCallback c) throws RemoteException {
             executeMethodAsync(() -> ImsRegistrationImplBase.this.removeRegistrationCallback(c),
                     "removeRegistrationCallback");
@@ -302,16 +328,23 @@
 
     private final RemoteCallbackListExt<IImsRegistrationCallback> mCallbacks =
             new RemoteCallbackListExt<>();
+    private final RemoteCallbackListExt<IImsRegistrationCallback> mEmergencyCallbacks =
+            new RemoteCallbackListExt<>();
     private final Object mLock = new Object();
     // Locked on mLock
     private ImsRegistrationAttributes mRegistrationAttributes;
+    private ImsRegistrationAttributes mEmergencyRegistrationAttributes;
     // Locked on mLock
     private int mRegistrationState = REGISTRATION_STATE_UNKNOWN;
+    private int mEmergencyRegistrationState = REGISTRATION_STATE_UNKNOWN;
     // Locked on mLock, create unspecified disconnect cause.
     private ImsReasonInfo mLastDisconnectCause = new ImsReasonInfo();
+    private ImsReasonInfo mEmergencyLastDisconnectCause = new ImsReasonInfo();
     // Locked on mLock
     private int mLastDisconnectSuggestedAction = RegistrationManager.SUGGESTED_ACTION_NONE;
+    private int mEmergencyLastDisconnectSuggestedAction = RegistrationManager.SUGGESTED_ACTION_NONE;
     private int mLastDisconnectRadioTech = REGISTRATION_TECH_NONE;
+    private int mEmergencyLastDisconnectRadioTech = REGISTRATION_TECH_NONE;
 
     // We hold onto the uris each time they change so that we can send it to a callback when its
     // first added.
@@ -331,7 +364,7 @@
         // do not want to risk introducing a deadlock by using the same mCallbacks Object to
         // synchronize on outgoing and incoming operations.
         mCallbacks.register(c);
-        updateNewCallbackWithState(c);
+        updateNewCallbackWithState(c, false);
     }
 
     private void removeRegistrationCallback(IImsRegistrationCallback c) {
@@ -342,6 +375,16 @@
         mCallbacks.unregister(c);
     }
 
+    private void addEmergencyRegistrationCallback(IImsRegistrationCallback c)
+            throws RemoteException {
+        mEmergencyCallbacks.register(c);
+        updateNewCallbackWithState(c, true);
+    }
+
+    private void removeEmergencyRegistrationCallback(IImsRegistrationCallback c) {
+        mEmergencyCallbacks.unregister(c);
+    }
+
     /**
      * Called by the framework to request that the ImsService perform the network registration
      * of all SIP delegates associated with this ImsService.
@@ -428,14 +471,19 @@
      */
     @SystemApi
     public final void onRegistered(@NonNull ImsRegistrationAttributes attributes) {
-        updateToState(attributes, RegistrationManager.REGISTRATION_STATE_REGISTERED);
+        boolean isEmergency = isEmergency(attributes);
+        if (isEmergency) {
+            updateToEmergencyState(attributes, RegistrationManager.REGISTRATION_STATE_REGISTERED);
+        } else {
+            updateToState(attributes, RegistrationManager.REGISTRATION_STATE_REGISTERED);
+        }
         broadcastToCallbacksLocked((c) -> {
             try {
                 c.onRegistered(attributes);
             } catch (RemoteException e) {
                 Log.w(LOG_TAG, e + "onRegistered(int, Set) - Skipping callback.");
             }
-        });
+        }, isEmergency);
     }
 
     /**
@@ -457,14 +505,19 @@
      */
     @SystemApi
     public final void onRegistering(@NonNull ImsRegistrationAttributes attributes) {
-        updateToState(attributes, RegistrationManager.REGISTRATION_STATE_REGISTERING);
+        boolean isEmergency = isEmergency(attributes);
+        if (isEmergency) {
+            updateToEmergencyState(attributes, RegistrationManager.REGISTRATION_STATE_REGISTERING);
+        } else {
+            updateToState(attributes, RegistrationManager.REGISTRATION_STATE_REGISTERING);
+        }
         broadcastToCallbacksLocked((c) -> {
             try {
                 c.onRegistering(attributes);
             } catch (RemoteException e) {
                 Log.w(LOG_TAG, e + "onRegistering(int, Set) - Skipping callback.");
             }
-        });
+        }, isEmergency);
     }
 
     /**
@@ -513,16 +566,57 @@
     public final void onDeregistered(@Nullable ImsReasonInfo info,
             @RegistrationManager.SuggestedAction int suggestedAction,
             @ImsRegistrationTech int imsRadioTech) {
-        updateToDisconnectedState(info, suggestedAction, imsRadioTech);
+        // Impl to keep backwards compatibility with old implementations
+        ImsRegistrationAttributes attributes = mRegistrationAttributes != null
+                ? new ImsRegistrationAttributes(imsRadioTech,
+                        mRegistrationAttributes.getTransportType(),
+                        mRegistrationAttributes.getAttributeFlags(),
+                        mRegistrationAttributes.getFeatureTags()) :
+                new ImsRegistrationAttributes.Builder(imsRadioTech).build();
+        onDeregistered(info, suggestedAction, attributes);
+    }
+
+    /**
+     * Notify the framework that the device is disconnected from the IMS network.
+     * <p>
+     * Note: Prior to calling {@link #onDeregistered(ImsReasonInfo,int)}, you should ensure that any
+     * changes to {@link android.telephony.ims.feature.ImsFeature} capability availability is sent
+     * to the framework.  For example,
+     * {@link android.telephony.ims.feature.MmTelFeature.MmTelCapabilities#CAPABILITY_TYPE_VIDEO}
+     * and
+     * {@link android.telephony.ims.feature.MmTelFeature.MmTelCapabilities#CAPABILITY_TYPE_VOICE}
+     * may be set to unavailable to ensure the framework knows these services are no longer
+     * available due to de-registration.  If you do not report capability changes impacted by
+     * de-registration, the framework will not know which features are no longer available as a
+     * result.
+     *
+     * @param info the {@link ImsReasonInfo} associated with why registration was disconnected.
+     * @param suggestedAction the expected behavior of radio protocol stack.
+     * @param attributes The attributes associated with the IMS registration
+     * @hide This API is not part of the Android public SDK API
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+    public final void onDeregistered(@Nullable ImsReasonInfo info,
+                                     @RegistrationManager.SuggestedAction int suggestedAction,
+                                     @NonNull ImsRegistrationAttributes attributes) {
+        boolean isEmergency = isEmergency(attributes);
+        int imsRadioTech = attributes.getRegistrationTechnology();
+        if (isEmergency) {
+            updateToDisconnectedEmergencyState(info, suggestedAction, imsRadioTech);
+        } else {
+            updateToDisconnectedState(info, suggestedAction, imsRadioTech);
+        }
         // ImsReasonInfo should never be null.
         final ImsReasonInfo reasonInfo = (info != null) ? info : new ImsReasonInfo();
+
         broadcastToCallbacksLocked((c) -> {
             try {
                 c.onDeregistered(reasonInfo, suggestedAction, imsRadioTech);
             } catch (RemoteException e) {
                 Log.w(LOG_TAG, e + "onDeregistered() - Skipping callback.");
             }
-        });
+        }, isEmergency);
     }
 
     /**
@@ -568,6 +662,7 @@
      *
      * @param info the {@link ImsReasonInfo} associated with why registration was disconnected.
      * @param suggestedAction the expected behavior of radio protocol stack.
+     * @param imsRadioTech the network type on which IMS registration has failed.
      * @param details the {@link SipDetails} related to disconnected Ims registration
      * @hide This API is not part of the Android public SDK API
      */
@@ -584,7 +679,7 @@
             } catch (RemoteException e) {
                 Log.w(LOG_TAG, e + "onDeregistered() - Skipping callback.");
             }
-        });
+        }, false);
     }
 
     /**
@@ -599,14 +694,38 @@
     @SystemApi
     public final void onTechnologyChangeFailed(@ImsRegistrationTech int imsRadioTech,
             ImsReasonInfo info) {
+        ImsRegistrationAttributes attributes = mRegistrationAttributes != null
+                ? new ImsRegistrationAttributes(imsRadioTech,
+                        mRegistrationAttributes.getTransportType(),
+                        mRegistrationAttributes.getAttributeFlags(),
+                        mRegistrationAttributes.getFeatureTags()) :
+                new ImsRegistrationAttributes.Builder(imsRadioTech).build();
+        onTechnologyChangeFailed(info, attributes);
+    }
+
+    /**
+     * Notify the framework that the handover from the current radio technology to the technology
+     * defined in {@code imsRadioTech} has failed.
+     * {@link #REGISTRATION_TECH_LTE}, {@link #REGISTRATION_TECH_IWLAN} and
+     * {@link #REGISTRATION_TECH_CROSS_SIM}.
+     * @param info The {@link ImsReasonInfo} for the failure to change technology.
+     * @param attributes The attributes associated with the IMS registration
+     * @hide This API is not part of the Android public SDK API
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_EMERGENCY_REGISTRATION_STATE)
+    public final void onTechnologyChangeFailed(@Nullable ImsReasonInfo info,
+                                               @NonNull ImsRegistrationAttributes attributes) {
+        boolean isEmergency = isEmergency(attributes);
+        int imsRadioTech = attributes.getRegistrationTechnology();
         final ImsReasonInfo reasonInfo = (info != null) ? info : new ImsReasonInfo();
-        broadcastToCallbacksLocked((c) -> {
+        broadcastToCallbacksLocked(c -> {
             try {
                 c.onTechnologyChangeFailed(imsRadioTech, reasonInfo);
             } catch (RemoteException e) {
                 Log.w(LOG_TAG, e + "onTechnologyChangeFailed() - Skipping callback.");
             }
-        });
+        }, isEmergency);
     }
 
     /**
@@ -623,19 +742,35 @@
             mUris = ArrayUtils.cloneOrNull(uris);
             mUrisSet = true;
         }
-        broadcastToCallbacksLocked((c) -> onSubscriberAssociatedUriChanged(c, uris));
+        broadcastToCallbacksLocked((c) -> onSubscriberAssociatedUriChanged(c, uris), false);
+    }
+
+    private boolean isEmergency(ImsRegistrationAttributes attributes) {
+        if (attributes == null) {
+            return false;
+        } else {
+            return (attributes.getAttributeFlags()
+                    & ImsRegistrationAttributes.ATTR_REGISTRATION_TYPE_EMERGENCY) != 0;
+        }
     }
 
     /**
-     * Broadcast the specified operation in a synchronized manner so that multiple threads do not
+     * Broadcast the specified operation in ta synchronized manner so that multiple threads do not
      * try to call broadcast at the same time, which will generate an error.
      * @param c The Consumer lambda method containing the callback to call.
      */
-    private void broadcastToCallbacksLocked(Consumer<IImsRegistrationCallback> c) {
+    private void broadcastToCallbacksLocked(Consumer<IImsRegistrationCallback> c,
+                                            boolean isEmergency) {
         // One broadcast can happen at a time, so synchronize threads so only one
         // beginBroadcast/endBroadcast happens at a time.
-        synchronized (mCallbacks) {
-            mCallbacks.broadcastAction(c);
+        if (isEmergency) {
+            synchronized (mEmergencyCallbacks) {
+                mEmergencyCallbacks.broadcastAction(c);
+            }
+        } else {
+            synchronized (mCallbacks) {
+                mCallbacks.broadcastAction(c);
+            }
         }
     }
 
@@ -657,6 +792,16 @@
         }
     }
 
+    private void updateToEmergencyState(ImsRegistrationAttributes attributes, int newState) {
+        synchronized (mLock) {
+            mEmergencyRegistrationAttributes = attributes;
+            mEmergencyRegistrationState = newState;
+            mEmergencyLastDisconnectCause = null;
+            mEmergencyLastDisconnectSuggestedAction = RegistrationManager.SUGGESTED_ACTION_NONE;
+            mEmergencyLastDisconnectRadioTech = REGISTRATION_TECH_NONE;
+        }
+    }
+
     private void updateToDisconnectedState(ImsReasonInfo info,
             @RegistrationManager.SuggestedAction int suggestedAction,
             @ImsRegistrationTech int imsRadioTech) {
@@ -678,11 +823,33 @@
         }
     }
 
+    private void updateToDisconnectedEmergencyState(ImsReasonInfo info,
+                 @RegistrationManager.SuggestedAction int suggestedAction,
+                 @ImsRegistrationTech int imsRadioTech) {
+        synchronized (mLock) {
+            //We don't want to send this info over if we are disconnected
+            mUrisSet = false;
+            mUris = null;
+
+            updateToEmergencyState(new ImsRegistrationAttributes.Builder(REGISTRATION_TECH_NONE)
+                            .build(),
+                    RegistrationManager.REGISTRATION_STATE_NOT_REGISTERED);
+            if (info != null) {
+                mEmergencyLastDisconnectCause = info;
+                mEmergencyLastDisconnectSuggestedAction = suggestedAction;
+                mEmergencyLastDisconnectRadioTech = imsRadioTech;
+            } else {
+                Log.w(LOG_TAG, "updateToDisconnectedState: no ImsReasonInfo provided.");
+                mEmergencyLastDisconnectCause = new ImsReasonInfo();
+            }
+        }
+    }
+
     /**
      * @param c the newly registered callback that will be updated with the current registration
      *         state.
      */
-    private void updateNewCallbackWithState(IImsRegistrationCallback c)
+    private void updateNewCallbackWithState(IImsRegistrationCallback c, boolean isEmergencyCallback)
             throws RemoteException {
         int state;
         ImsRegistrationAttributes attributes;
@@ -692,11 +859,15 @@
         boolean urisSet;
         Uri[] uris;
         synchronized (mLock) {
-            state = mRegistrationState;
-            attributes = mRegistrationAttributes;
-            disconnectInfo = mLastDisconnectCause;
-            suggestedAction = mLastDisconnectSuggestedAction;
-            imsDisconnectRadioTech = mLastDisconnectRadioTech;
+            state = isEmergencyCallback ? mEmergencyRegistrationState : mRegistrationState;
+            attributes = isEmergencyCallback ? mEmergencyRegistrationAttributes :
+                    mRegistrationAttributes;
+            disconnectInfo = isEmergencyCallback ? mEmergencyLastDisconnectCause :
+                    mLastDisconnectCause;
+            suggestedAction = isEmergencyCallback ? mEmergencyLastDisconnectSuggestedAction :
+                    mLastDisconnectSuggestedAction;
+            imsDisconnectRadioTech = isEmergencyCallback ? mEmergencyLastDisconnectRadioTech :
+                    mLastDisconnectRadioTech;
             urisSet = mUrisSet;
             uris = mUris;
         }
diff --git a/telephony/java/com/android/internal/telephony/ISms.aidl b/telephony/java/com/android/internal/telephony/ISms.aidl
index 0e23f36..799ac64 100644
--- a/telephony/java/com/android/internal/telephony/ISms.aidl
+++ b/telephony/java/com/android/internal/telephony/ISms.aidl
@@ -592,4 +592,17 @@
      * @hide
      */
     boolean resetAllCellBroadcastRanges(int subId);
+
+    /**
+     * Gets the message size of a WAP from the cache.
+     *
+     * @param locationUrl the location to use as a key for looking up the size in the cache.
+     * The locationUrl may or may not have the transactionId appended to the url.
+     *
+     * @return long representing the message size
+     * @throws java.util.NoSuchElementException if the WAP push doesn't exist in the cache
+     *
+     * @hide
+     */
+    long getWapMessageSize(String locationUrl);
 }
diff --git a/telephony/java/com/android/internal/telephony/ISmsImplBase.java b/telephony/java/com/android/internal/telephony/ISmsImplBase.java
index 6864556..70bc4dd 100644
--- a/telephony/java/com/android/internal/telephony/ISmsImplBase.java
+++ b/telephony/java/com/android/internal/telephony/ISmsImplBase.java
@@ -227,4 +227,10 @@
     public boolean resetAllCellBroadcastRanges(int subId) {
         throw new UnsupportedOperationException();
     }
+
+    @Override
+    public long getWapMessageSize(String locationUrl) {
+        throw new UnsupportedOperationException();
+    }
+
 }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 37752d0..84777c9 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -1901,6 +1901,16 @@
     void unregisterImsRegistrationCallback(int subId, IImsRegistrationCallback c);
 
     /**
+     * Adds an IMS emergency registration status callback for the subscription id specified.
+     */
+    void registerImsEmergencyRegistrationCallback(int subId, IImsRegistrationCallback c);
+    /**
+     * Removes an existing IMS emergency registration status callback for the subscription
+     * id specified.
+     */
+    void unregisterImsEmergencyRegistrationCallback(int subId, IImsRegistrationCallback c);
+
+    /**
      * Get the IMS service registration state for the MmTelFeature associated with this sub id.
      */
     void getImsMmTelRegistrationState(int subId, IIntegerConsumer consumer);
@@ -3035,8 +3045,8 @@
      *
      * @param handoverType The type of handover from emergency call to satellite messaging. Use one
      *                     of the following values to enable the override:
-     *                     0 - EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_SOS
-     *                     1 - EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911
+     *                     1 - EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_SOS
+     *                     2 - EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911
      *                     To disable the override, use -1 for handoverType.
      * @param delaySeconds The event EVENT_DISPLAY_EMERGENCY_MESSAGE will be sent to Dialer
      *                     delaySeconds after the emergency call starts.
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index 5059017..b7dd4df 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -548,6 +548,7 @@
     int RIL_REQUEST_SET_CELLULAR_IDENTIFIER_DISCLOSED_ENABLED = 247;
     int RIL_REQUEST_SET_SECURITY_ALGORITHMS_UPDATED_ENABLED = 248;
     int RIL_REQUEST_IS_SECURITY_ALGORITHMS_UPDATED_ENABLED = 249;
+    int RIL_REQUEST_GET_SIMULTANEOUS_CALLING_SUPPORT = 250;
 
     /* Responses begin */
     int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
@@ -625,4 +626,5 @@
     int RIL_UNSOL_CONNECTION_SETUP_FAILURE = 1108;
     int RIL_UNSOL_NOTIFY_ANBR = 1109;
     int RIL_UNSOL_IMEI_MAPPING_CHANGED = 1110;
+    int RIL_UNSOL_SIMULTANEOUS_CALLING_SUPPORT_CHANGED = 1111;
 }
diff --git a/tests/ActivityManagerPerfTests/tests/Android.bp b/tests/ActivityManagerPerfTests/tests/Android.bp
index e5813ae..cce40f3a 100644
--- a/tests/ActivityManagerPerfTests/tests/Android.bp
+++ b/tests/ActivityManagerPerfTests/tests/Android.bp
@@ -30,6 +30,12 @@
         "ActivityManagerPerfTestsUtils",
         "collector-device-lib-platform",
     ],
+    data: [
+        ":ActivityManagerPerfTestsTestApp",
+        ":ActivityManagerPerfTestsStubApp1",
+        ":ActivityManagerPerfTestsStubApp2",
+        ":ActivityManagerPerfTestsStubApp3",
+    ],
     platform_apis: true,
     min_sdk_version: "25",
     // For android.permission.FORCE_STOP_PACKAGES permission
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index c49f8fe..be47ec7 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
+import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.traces.parsers.toFlickerComponent
-import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
@@ -58,9 +58,10 @@
         }
         transitions {
             broadcastActionTrigger.doAction(ACTION_FINISH_ACTIVITY)
-            wmHelper.StateSyncBuilder()
-                    .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent())
-                    .waitForAndVerify()
+            wmHelper
+                .StateSyncBuilder()
+                .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent())
+                .waitForAndVerify()
         }
         teardown { simpleApp.exit(wmHelper) }
     }
@@ -69,10 +70,16 @@
 
     @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
-    @FlakyTest(bugId = 246284124)
+    @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS.toMutableList().also {
+                    it.add(simpleApp.componentMatcher)
+                }
+            )
+        }
     }
 
     @Presubmit
diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java
index 99b303e..37b39c3 100644
--- a/tests/testables/src/android/testing/TestWithLooperRule.java
+++ b/tests/testables/src/android/testing/TestWithLooperRule.java
@@ -19,7 +19,6 @@
 import android.testing.TestableLooper.LooperFrameworkMethod;
 import android.testing.TestableLooper.RunWithLooper;
 
-import org.junit.internal.runners.statements.InvokeMethod;
 import org.junit.rules.MethodRule;
 import org.junit.runner.RunWith;
 import org.junit.runners.model.FrameworkMethod;
@@ -79,13 +78,11 @@
             while (next != null) {
                 switch (next.getClass().getSimpleName()) {
                     case "RunAfters":
-                        this.<List<FrameworkMethod>>wrapFieldMethodFor(next,
-                                next.getClass(), "afters", method, target);
+                        this.wrapFieldMethodFor(next, "afters", method, target);
                         next = getNextStatement(next, "next");
                         break;
                     case "RunBefores":
-                        this.<List<FrameworkMethod>>wrapFieldMethodFor(next,
-                                next.getClass(), "befores", method, target);
+                        this.wrapFieldMethodFor(next, "befores", method, target);
                         next = getNextStatement(next, "next");
                         break;
                     case "FailOnTimeout":
@@ -95,8 +92,10 @@
                         next = getNextStatement(next, "originalStatement");
                         break;
                     case "InvokeMethod":
-                        this.<FrameworkMethod>wrapFieldMethodFor(next,
-                                InvokeMethod.class, "testMethod", method, target);
+                        this.wrapFieldMethodFor(next, "testMethod", method, target);
+                        return;
+                    case "InvokeParameterizedMethod":
+                        this.wrapFieldMethodFor(next, "frameworkMethod", method, target);
                         return;
                     default:
                         throw new Exception(
@@ -112,12 +111,11 @@
 
     // Wrapping the befores, afters, and InvokeMethods with LooperFrameworkMethod
     // within the statement.
-    private <T> void wrapFieldMethodFor(Statement base, Class<?> targetClass, String fieldStr,
-            FrameworkMethod method, Object target)
-            throws NoSuchFieldException, IllegalAccessException {
-        Field field = targetClass.getDeclaredField(fieldStr);
+    private void wrapFieldMethodFor(Statement base, String fieldStr, FrameworkMethod method,
+            Object target) throws NoSuchFieldException, IllegalAccessException {
+        Field field = base.getClass().getDeclaredField(fieldStr);
         field.setAccessible(true);
-        T fieldInstance = (T) field.get(base);
+        Object fieldInstance = field.get(base);
         if (fieldInstance instanceof FrameworkMethod) {
             field.set(base, looperWrap(method, target, (FrameworkMethod) fieldInstance));
         } else {
diff --git a/tools/hoststubgen/README.md b/tools/hoststubgen/README.md
index 3455b0a..1a895dc 100644
--- a/tools/hoststubgen/README.md
+++ b/tools/hoststubgen/README.md
@@ -34,11 +34,6 @@
 
   - `test-tiny-framework/` See `README.md` in it.
 
-  - `test-framework`
-    This directory was used during the prototype phase, but now that we have real ravenwood tests,
-    this directory is obsolete and should be deleted.
-
-
 - `scripts`
   - `dump-jar.sh`
 
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index 0e2e158..57bcc04 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -9,7 +9,7 @@
 
 // Visibility only for ravenwood prototype uses.
 genrule_defaults {
-    name: "hoststubgen-for-prototype-only-genrule",
+    name: "ravenwood-internal-only-visibility-genrule",
     visibility: [
         ":__subpackages__",
         "//frameworks/base",
@@ -19,7 +19,7 @@
 
 // Visibility only for ravenwood prototype uses.
 java_defaults {
-    name: "hoststubgen-for-prototype-only-java",
+    name: "ravenwood-internal-only-visibility-java",
     visibility: [
         ":__subpackages__",
         "//frameworks/base",
@@ -29,7 +29,7 @@
 
 // Visibility only for ravenwood prototype uses.
 filegroup_defaults {
-    name: "hoststubgen-for-prototype-only-filegroup",
+    name: "ravenwood-internal-only-visibility-filegroup",
     visibility: [
         ":__subpackages__",
         "//frameworks/base",
@@ -41,7 +41,7 @@
 // This is only for the prototype. The productionized version is "ravenwood-annotations".
 java_library {
     name: "hoststubgen-annotations",
-    defaults: ["hoststubgen-for-prototype-only-java"],
+    defaults: ["ravenwood-internal-only-visibility-java"],
     srcs: [
         "annotations-src/**/*.java",
     ],
@@ -115,7 +115,7 @@
 // This is only for the prototype. The productionized version is "ravenwood-standard-options".
 filegroup {
     name: "hoststubgen-standard-options",
-    defaults: ["hoststubgen-for-prototype-only-filegroup"],
+    defaults: ["ravenwood-internal-only-visibility-filegroup"],
     srcs: [
         "hoststubgen-standard-options.txt",
     ],
@@ -153,146 +153,25 @@
     ],
 }
 
-// Generate the stub/impl from framework-all, with hidden APIs.
-java_genrule_host {
-    name: "framework-all-hidden-api-host",
-    defaults: ["hoststubgen-command-defaults"],
-    cmd: hoststubgen_common_options +
-        "--in-jar $(location :framework-all) " +
-        "--policy-override-file $(location framework-policy-override.txt) ",
-    srcs: [
-        ":framework-all",
-        "framework-policy-override.txt",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-// Extract the stub jar from "framework-all-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-hidden-api-host-stub",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-hidden-api-host{host_stub.jar}",
-    ],
-    out: [
-        "host_stub.jar",
-    ],
-}
-
-// Extract the impl jar from "framework-all-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-hidden-api-host-impl",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-hidden-api-host{host_impl.jar}",
-    ],
-    out: [
-        "host_impl.jar",
-    ],
-}
-
-// Generate the stub/impl from framework-all, with only public/system/test APIs, without
-// hidden APIs.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-test-api-host",
-    defaults: ["hoststubgen-command-defaults"],
-    cmd: hoststubgen_common_options +
-        "--intersect-stub-jar $(location :android_test_stubs_current{.jar}) " +
-        "--in-jar $(location :framework-all) " +
-        "--policy-override-file $(location framework-policy-override.txt) ",
-    srcs: [
-        ":framework-all",
-        ":android_test_stubs_current{.jar}",
-        "framework-policy-override.txt",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-// Extract the stub jar from "framework-all-test-api-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-test-api-host-stub",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-test-api-host{host_stub.jar}",
-    ],
-    out: [
-        "host_stub.jar",
-    ],
-}
-
-// Extract the impl jar from "framework-all-test-api-host" for subsequent build rules.
-// This is only for the prototype. Do not use it in "productionized" build rules.
-java_genrule_host {
-    name: "framework-all-test-api-host-impl",
-    defaults: ["hoststubgen-for-prototype-only-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":framework-all-test-api-host{host_impl.jar}",
-    ],
-    out: [
-        "host_impl.jar",
-    ],
-}
-
-// This library contains helper classes to build hostside tests/targets.
-// This essentially contains dependencies from tests that we can't actually use the real ones.
-// For example, the actual AndroidTestCase and AndroidJUnit4 don't run on the host side (yet),
-// so we pup "fake" implementations here.
-// Ideally this library should be empty.
-java_library_host {
-    name: "hoststubgen-helper-framework-buildtime",
-    defaults: ["hoststubgen-for-prototype-only-java"],
-    srcs: [
-        "helper-framework-buildtime-src/**/*.java",
-    ],
-    libs: [
-        // We need it to pull in some of the framework classes used in this library,
-        // such as Context.java.
-        "framework-all-hidden-api-host-impl",
-        "junit",
-    ],
-}
-
-// This module contains "fake" libcore/dalvik classes, framework native substitution, etc,
-// that are needed at runtime.
-java_library_host {
-    name: "hoststubgen-helper-framework-runtime",
-    defaults: ["hoststubgen-for-prototype-only-java"],
-    srcs: [
-        "helper-framework-runtime-src/**/*.java",
-    ],
-    libs: [
-        "hoststubgen-helper-runtime",
-        "framework-all-hidden-api-host-impl",
-    ],
-}
-
 java_library_host {
     name: "hoststubgen-helper-libcore-runtime",
-    defaults: ["hoststubgen-for-prototype-only-java"],
     srcs: [
         "helper-framework-runtime-src/libcore-fake/**/*.java",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_host_for_device {
     name: "hoststubgen-helper-libcore-runtime.ravenwood",
-    defaults: ["hoststubgen-for-prototype-only-java"],
     libs: [
         "hoststubgen-helper-libcore-runtime",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_library {
     name: "hoststubgen-helper-framework-runtime.ravenwood",
-    defaults: ["hoststubgen-for-prototype-only-java"],
+    defaults: ["ravenwood-internal-only-visibility-java"],
     srcs: [
         "helper-framework-runtime-src/framework/**/*.java",
     ],
@@ -305,88 +184,3 @@
         "hoststubgen-helper-libcore-runtime.ravenwood",
     ],
 }
-
-// Defaults for host side test modules.
-// We need two rules for each test.
-// 1. A "-test-lib" jar, which compiles the test against the stub jar.
-//    This one is only used by the second rule, so it should be "private.
-// 2. A "-test" jar, which includes 1 + the runtime (impl) jars.
-
-// This and next ones are for tests using framework-app, with hidden APIs.
-java_defaults {
-    name: "hosttest-with-framework-all-hidden-api-test-lib-defaults",
-    installable: false,
-    libs: [
-        "framework-all-hidden-api-host-stub",
-    ],
-    static_libs: [
-        "hoststubgen-helper-framework-buildtime",
-        "framework-annotations-lib",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-// Default rules to include `libandroid_runtime`. For now, it's empty, but we'll use it
-// once we start using JNI.
-java_defaults {
-    name: "hosttest-with-libandroid_runtime",
-    jni_libs: [
-        // "libandroid_runtime",
-
-        // TODO: Figure out how to build them automatically.
-        // Following ones are depended by libandroid_runtime.
-        // Without listing them here, not only we won't get them under
-        // $ANDROID_HOST_OUT/testcases/*/lib64, but also not under
-        // $ANDROID_HOST_OUT/lib64, so we'd fail to load them at runtime.
-        // ($ANDROID_HOST_OUT/lib/ gets all of them though.)
-        // "libcutils",
-        // "libharfbuzz_ng",
-        // "libminikin",
-        // "libz",
-        // "libbinder",
-        // "libhidlbase",
-        // "libvintf",
-        // "libicu",
-        // "libutils",
-        // "libtinyxml2",
-    ],
-}
-
-java_defaults {
-    name: "hosttest-with-framework-all-hidden-api-test-defaults",
-    defaults: ["hosttest-with-libandroid_runtime"],
-    installable: false,
-    test_config: "AndroidTest-host.xml",
-    static_libs: [
-        "hoststubgen-helper-runtime",
-        "hoststubgen-helper-framework-runtime",
-        "framework-all-hidden-api-host-impl",
-    ],
-}
-
-// This and next ones are for tests using framework-app, with public/system/test APIs,
-// without hidden APIs.
-java_defaults {
-    name: "hosttest-with-framework-all-test-api-test-lib-defaults",
-    installable: false,
-    libs: [
-        "framework-all-test-api-host-stub",
-    ],
-    static_libs: [
-        "hoststubgen-helper-framework-buildtime",
-        "framework-annotations-lib",
-    ],
-    visibility: ["//visibility:private"],
-}
-
-java_defaults {
-    name: "hosttest-with-framework-all-test-api-test-defaults",
-    defaults: ["hosttest-with-libandroid_runtime"],
-    installable: false,
-    test_config: "AndroidTest-host.xml",
-    static_libs: [
-        "hoststubgen-helper-runtime",
-        "hoststubgen-helper-framework-runtime",
-        "framework-all-test-api-host-impl",
-    ],
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java
deleted file mode 100644
index e6d3866..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/android/test/AndroidTestCase.java
+++ /dev/null
@@ -1,27 +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 android.test;
-
-import android.content.Context;
-
-import junit.framework.TestCase;
-
-public class AndroidTestCase extends TestCase {
-    protected Context mContext;
-    public Context getContext() {
-        throw new RuntimeException("[ravenwood] Class Context is not supported yet.");
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java
deleted file mode 100644
index 51c5d9a..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/NonNull.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2013 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 androidx.annotation;
-
-// [ravenwood] TODO: Find the actual androidx jar containing it.s
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * Denotes that a parameter, field or method return value can never be null.
- * <p>
- * This is a marker annotation and it has no specific attributes.
- *
- * @paramDoc This value cannot be {@code null}.
- * @returnDoc This value cannot be {@code null}.
- * @hide
- */
-@Retention(SOURCE)
-@Target({METHOD, PARAMETER, FIELD})
-public @interface NonNull {
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java
deleted file mode 100644
index f1f0e8b..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/annotation/Nullable.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2013 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 androidx.annotation;
-
-// [ravenwood] TODO: Find the actual androidx jar containing it.s
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * Denotes that a parameter, field or method return value can be null.
- * <p>
- * When decorating a method call parameter, this denotes that the parameter can
- * legitimately be null and the method will gracefully deal with it. Typically
- * used on optional parameters.
- * <p>
- * When decorating a method, this denotes the method might legitimately return
- * null.
- * <p>
- * This is a marker annotation and it has no specific attributes.
- *
- * @paramDoc This value may be {@code null}.
- * @returnDoc This value may be {@code null}.
- * @hide
- */
-@Retention(SOURCE)
-@Target({METHOD, PARAMETER, FIELD})
-public @interface Nullable {
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java
deleted file mode 100644
index 0c82e4e..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/ext/junit/runners/AndroidJUnit4.java
+++ /dev/null
@@ -1,30 +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 androidx.test.ext.junit.runners;
-
-import org.junit.runners.BlockJUnit4ClassRunner;
-import org.junit.runners.model.InitializationError;
-
-// TODO: We need to simulate the androidx test runner.
-// https://source.corp.google.com/piper///depot/google3/third_party/android/androidx_test/ext/junit/java/androidx/test/ext/junit/runners/AndroidJUnit4.java
-// https://source.corp.google.com/piper///depot/google3/third_party/android/androidx_test/runner/android_junit_runner/java/androidx/test/internal/runner/junit4/AndroidJUnit4ClassRunner.java
-
-public class AndroidJUnit4 extends BlockJUnit4ClassRunner {
-    public AndroidJUnit4(Class<?> testClass) throws InitializationError {
-        super(testClass);
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java
deleted file mode 100644
index 2470d839..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/FlakyTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2014 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 androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Designates a test as being flaky (non-deterministic).
- *
- * <p>Can then be used to filter tests on execution using -e annotation or -e notAnnotation as
- * desired.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface FlakyTest {
-  /**
-   * An optional bug number associated with the test. -1 Means that no bug number is associated with
-   * the flaky annotation.
-   *
-   * @return int
-   */
-  int bugId() default -1;
-
-  /**
-   * Details, such as the reason of why the test is flaky.
-   *
-   * @return String
-   */
-  String detail() default "";
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java
deleted file mode 100644
index 578d7dc..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/LargeTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation to assign a large test size qualifier to a test. This annotation can be used at a
- * method or class level.
- *
- * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a
- * test suite of similar run time.
- *
- * <p>Execution time: &gt;1000ms
- *
- * <p>Large tests should be focused on testing integration of all application components. These
- * tests fully participate in the system and may make use of all resources such as databases, file
- * systems and network. As a rule of thumb most functional UI tests are large tests.
- *
- * <p>Note: This class replaces the deprecated Android platform size qualifier <a
- * href="{@docRoot}reference/android/test/suitebuilder/annotation/LargeTest.html"><code>
- * android.test.suitebuilder.annotation.LargeTest</code></a> and is the recommended way to annotate
- * tests written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface LargeTest {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java
deleted file mode 100644
index dfdaa53..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/MediumTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation to assign a medium test size qualifier to a test. This annotation can be used at a
- * method or class level.
- *
- * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a
- * test suite of similar run time.
- *
- * <p>Execution time: &lt;1000ms
- *
- * <p>Medium tests should be focused on a very limited subset of components or a single component.
- * Resource access to the file system through well defined interfaces like databases,
- * ContentProviders, or Context is permitted. Network access should be restricted, (long-running)
- * blocking operations should be avoided and use mock objects instead.
- *
- * <p>Note: This class replaces the deprecated Android platform size qualifier <a
- * href="{@docRoot}reference/android/test/suitebuilder/annotation/MediumTest.html"><code>
- * android.test.suitebuilder.annotation.MediumTest</code></a> and is the recommended way to annotate
- * tests written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface MediumTest {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java
deleted file mode 100644
index 3d3ee33..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/RequiresDevice.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2014 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 androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Indicates that a specific test should not be run on emulator.
- *
- * <p>It will be executed only if the test is running on the physical android device.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
-public @interface RequiresDevice {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java
deleted file mode 100644
index dd65ddb..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SdkSuppress.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2014 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 androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Indicates that a specific test or class requires a minimum or maximum API Level to execute.
- *
- * <p>Test(s) will be skipped when executed on android platforms less/more than specified level
- * (inclusive).
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
-public @interface SdkSuppress {
-  /** The minimum API level to execute (inclusive) */
-  int minSdkVersion() default 1;
-  /** The maximum API level to execute (inclusive) */
-  int maxSdkVersion() default Integer.MAX_VALUE;
-  /**
-   * The {@link android.os.Build.VERSION.CODENAME} to execute on. This is intended to be used to run
-   * on a pre-release SDK, where the {@link android.os.Build.VERSION.SDK_INT} has not yet been
-   * finalized. This is treated as an OR operation with respect to the minSdkVersion and
-   * maxSdkVersion attributes.
-   *
-   * <p>For example, to filter a test so it runs on only the prerelease R SDK: <code>
-   * {@literal @}SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, codeName = "R")
-   * </code>
-   */
-  String codeName() default "unset";
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java
deleted file mode 100644
index dd32df4..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/SmallTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Annotation to assign a small test size qualifier to a test. This annotation can be used at a
- * method or class level.
- *
- * <p>Test size qualifiers are a great way to structure test code and are used to assign a test to a
- * test suite of similar run time.
- *
- * <p>Execution time: &lt;200ms
- *
- * <p>Small tests should be run very frequently. Focused on units of code to verify specific logical
- * conditions. These tests should runs in an isolated environment and use mock objects for external
- * dependencies. Resource access (such as file system, network, or databases) are not permitted.
- * Tests that interact with hardware, make binder calls, or that facilitate android instrumentation
- * should not use this annotation.
- *
- * <p>Note: This class replaces the deprecated Android platform size qualifier <a
- * href="http://developer.android.com/reference/android/test/suitebuilder/annotation/SmallTest.html">
- * android.test.suitebuilder.annotation.SmallTest</a> and is the recommended way to annotate tests
- * written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface SmallTest {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java
deleted file mode 100644
index 88e636c..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/filters/Suppress.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.test.filters;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Use this annotation on test classes or test methods that should not be included in a test suite.
- * If the annotation appears on the class then no tests in that class will be included. If the
- * annotation appears only on a test method then only that method will be excluded.
- *
- * <p>Note: This class replaces the deprecated Android platform annotation <a
- * href="http://developer.android.com/reference/android/test/suitebuilder/annotation/Suppress.html">
- * android.test.suitebuilder.annotation.Suppress</a> and is the recommended way to suppress tests
- * written with the AndroidX Test Library.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
-public @interface Suppress {}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java b/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java
deleted file mode 100644
index e137939..0000000
--- a/tools/hoststubgen/hoststubgen/helper-framework-buildtime-src/androidx/test/runner/AndroidJUnit4.java
+++ /dev/null
@@ -1,24 +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 androidx.test.runner;
-
-import org.junit.runners.model.InitializationError;
-
-public class AndroidJUnit4 extends androidx.test.ext.junit.runners.AndroidJUnit4 {
-    public AndroidJUnit4(Class<?> testClass) throws InitializationError {
-        super(testClass);
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
new file mode 100644
index 0000000..631fc02
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
@@ -0,0 +1,159 @@
+/*
+ * 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.hoststubgen.nativesubstitution;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.util.Base64;
+
+import java.text.DecimalFormat;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+public class CursorWindow_host {
+
+    private static final HashMap<Long, CursorWindow_host> sInstances = new HashMap<>();
+    private static long sNextId = 1;
+
+    private int mColumnNum;
+    private static class Row {
+        String[] fields;
+        int[] types;
+    }
+
+    private final List<Row> mRows = new ArrayList<>();
+
+    public static long nativeCreate(String name, int cursorWindowSize) {
+        CursorWindow_host instance = new CursorWindow_host();
+        long instanceId = sNextId++;
+        sInstances.put(instanceId, instance);
+        return instanceId;
+    }
+
+    public static void nativeDispose(long windowPtr) {
+        sInstances.remove(windowPtr);
+    }
+
+    public static boolean nativeSetNumColumns(long windowPtr, int columnNum) {
+        sInstances.get(windowPtr).mColumnNum = columnNum;
+        return true;
+    }
+
+    public static int nativeGetNumRows(long windowPtr) {
+        return sInstances.get(windowPtr).mRows.size();
+    }
+
+    public static boolean nativeAllocRow(long windowPtr) {
+        CursorWindow_host instance = sInstances.get(windowPtr);
+        Row row = new Row();
+        row.fields = new String[instance.mColumnNum];
+        row.types = new int[instance.mColumnNum];
+        Arrays.fill(row.types, Cursor.FIELD_TYPE_NULL);
+        instance.mRows.add(row);
+        return true;
+    }
+
+    private static boolean put(long windowPtr, String value, int type, int row, int column) {
+        CursorWindow_host instance = sInstances.get(windowPtr);
+        if (row >= instance.mRows.size() || column >= instance.mColumnNum) {
+            return false;
+        }
+        Row r = instance.mRows.get(row);
+        r.fields[column] = value;
+        r.types[column] = type;
+        return true;
+    }
+
+    public static int nativeGetType(long windowPtr, int row, int column) {
+        CursorWindow_host instance = sInstances.get(windowPtr);
+        if (row >= instance.mRows.size() || column >= instance.mColumnNum) {
+            return Cursor.FIELD_TYPE_NULL;
+        }
+
+        return instance.mRows.get(row).types[column];
+    }
+
+    public static boolean nativePutString(long windowPtr, String value,
+            int row, int column) {
+        return put(windowPtr, value, Cursor.FIELD_TYPE_STRING, row, column);
+    }
+
+    public static String nativeGetString(long windowPtr, int row, int column) {
+        CursorWindow_host instance = sInstances.get(windowPtr);
+        if (row >= instance.mRows.size() || column >= instance.mColumnNum) {
+            return null;
+        }
+
+        return instance.mRows.get(row).fields[column];
+    }
+
+    public static boolean nativePutLong(long windowPtr, long value, int row, int column) {
+        return put(windowPtr, Long.toString(value), Cursor.FIELD_TYPE_INTEGER, row, column);
+    }
+
+    public static long nativeGetLong(long windowPtr, int row, int column) {
+        String value = nativeGetString(windowPtr, row, column);
+        if (value == null) {
+            return 0;
+        }
+
+        Number number = new DecimalFormat().parse(value, new ParsePosition(0));
+        return number == null ? 0 : number.longValue();
+    }
+
+    public static boolean nativePutDouble(long windowPtr, double value, int row, int column) {
+        return put(windowPtr, Double.toString(value), Cursor.FIELD_TYPE_FLOAT, row, column);
+    }
+
+    public static double nativeGetDouble(long windowPtr, int row, int column) {
+        String value = nativeGetString(windowPtr, row, column);
+        if (value == null) {
+            return 0;
+        }
+
+        Number number = new DecimalFormat().parse(value, new ParsePosition(0));
+        return number == null ? 0 : number.doubleValue();
+    }
+
+    public static boolean nativePutBlob(long windowPtr, byte[] value, int row, int column) {
+        return put(windowPtr, value == null ? null : Base64.encodeToString(value, 0),
+                Cursor.FIELD_TYPE_BLOB, row, column);
+    }
+
+    public static byte[] nativeGetBlob(long windowPtr, int row, int column) {
+        int type = nativeGetType(windowPtr, row, column);
+        switch (type) {
+            case Cursor.FIELD_TYPE_BLOB: {
+                String value = nativeGetString(windowPtr, row, column);
+                return value == null ? null : Base64.decode(value, 0);
+            }
+            case Cursor.FIELD_TYPE_STRING: {
+                String value = nativeGetString(windowPtr, row, column);
+                return value == null ? null : value.getBytes();
+            }
+            case Cursor.FIELD_TYPE_FLOAT:
+                throw new SQLiteException();
+            case Cursor.FIELD_TYPE_INTEGER:
+                throw new SQLiteException();
+            case Cursor.FIELD_TYPE_NULL:
+            default:
+                return null;
+        }
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/test-framework/Android.bp b/tools/hoststubgen/hoststubgen/test-framework/Android.bp
deleted file mode 100644
index 2b91cc1..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/Android.bp
+++ /dev/null
@@ -1,19 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-build = ["AndroidHostTest.bp"]
diff --git a/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp b/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp
deleted file mode 100644
index 1f8382a..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/AndroidHostTest.bp
+++ /dev/null
@@ -1,57 +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.
-
-// Add `build = ["AndroidHostTest.bp"]` to Android.bp to include this file.
-
-// Compile the test jar, using 2 rules.
-// 1. Build the test against the stub.
-java_library_host {
-    name: "HostStubGenTest-framework-test-host-test-lib",
-    defaults: ["hosttest-with-framework-all-hidden-api-test-lib-defaults"],
-    srcs: [
-        "src/**/*.java",
-    ],
-    static_libs: [
-        "junit",
-        "truth",
-        "mockito",
-
-        // http://cs/h/googleplex-android/platform/superproject/main/+/main:platform_testing/libraries/annotations/src/android/platform/test/annotations/
-        "platform-test-annotations",
-        "hoststubgen-annotations",
-    ],
-}
-
-// 2. Link the above module with necessary runtime dependencies, so it can be executed stand-alone.
-java_test_host {
-    name: "HostStubGenTest-framework-all-test-host-test",
-    defaults: ["hosttest-with-framework-all-hidden-api-test-defaults"],
-    static_libs: [
-        "HostStubGenTest-framework-test-host-test-lib",
-    ],
-    test_suites: ["general-tests"],
-}
-
-// "Productionized" build rule.
-android_ravenwood_test {
-    name: "HostStubGenTest-framework-test",
-    srcs: [
-        "src/**/*.java",
-    ],
-    static_libs: [
-        "junit",
-        "truth",
-        "mockito",
-    ],
-}
diff --git a/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml b/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml
deleted file mode 100644
index f35dcf6..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/AndroidTest-host.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2023 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- [Ravenwood] Copied from $ANDROID_BUILD_TOP/cts/hostsidetests/devicepolicy/AndroidTest.xml  -->
-<configuration description="CtsContentTestCases host-side test">
-    <option name="test-suite-tag" value="ravenwood" />
-    <option name="config-descriptor:metadata" key="component" value="framework" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
-    <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" />
-
-    <test class="com.android.tradefed.testtype.IsolatedHostTest" >
-        <option name="jar" value="HostStubGenTest-framework-all-test-host-test.jar" />
-    </test>
-</configuration>
diff --git a/tools/hoststubgen/hoststubgen/test-framework/README.md b/tools/hoststubgen/hoststubgen/test-framework/README.md
deleted file mode 100644
index 26a9ad1..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# HostStubGen: (obsolete) real framework test
-
-This directory contains tests against the actual framework.jar code. The tests were
-copied from somewhere else in the android tree. We use this directory to quickly run existing
-tests.
-
-This directory was used during the prototype phase, but now that we have real ravenwood tests,
-this directory is obsolete and should be deleted.
-
-## How to run
-
-- With `atest`. This is the proper way to run it, but it may fail due to atest's known problems.
-
-```
-$ atest HostStubGenTest-framework-all-test-host-test
-```
-
-- Advanced option: `run-test-without-atest.sh` runs the test without using `atest`
-
-```
-$ ./run-test-without-atest.sh
-```
\ No newline at end of file
diff --git a/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh b/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh
deleted file mode 100755
index cfc06a1..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/run-test-without-atest.sh
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/bin/bash
-# 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.
-
-# Run HostStubGenTest-framework-test-host-test directly with JUnit.
-# (without using atest.)
-
-source "${0%/*}"/../../common.sh
-
-
-# Options:
-# -v enable verbose log
-# -d enable debugger
-
-verbose=0
-debug=0
-while getopts "vd" opt; do
-  case "$opt" in
-    v) verbose=1 ;;
-    d) debug=1 ;;
-  esac
-done
-shift $(($OPTIND - 1))
-
-
-if (( $verbose )) ; then
-  JAVA_OPTS="$JAVA_OPTS -verbose:class"
-fi
-
-if (( $debug )) ; then
-  JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8700"
-fi
-
-#=======================================
-module=HostStubGenTest-framework-all-test-host-test
-module_jar=$ANDROID_BUILD_TOP/out/host/linux-x86/testcases/$module/$module.jar
-run m $module
-
-out=out
-
-rm -fr $out
-mkdir -p $out
-
-
-# Copy and extract the relevant jar files so we can look into them.
-run cp \
-    $module_jar \
-    $SOONG_INT/frameworks/base/tools/hoststubgen/hoststubgen/framework-all-hidden-api-host/linux_glibc_common/gen/*.jar \
-    $out
-
-run extract $out/*.jar
-
-# Result is the number of failed tests.
-result=0
-
-
-# This suite runs all tests in the JAR.
-tests=(com.android.hoststubgen.hosthelper.HostTestSuite)
-
-# Uncomment this to run a specific test.
-# tests=(com.android.hoststubgen.frameworktest.LogTest)
-
-
-for class in ${tests[@]} ; do
-  echo "Running $class ..."
-
-  run cd "${module_jar%/*}"
-  run $JAVA $JAVA_OPTS \
-      -cp $module_jar \
-      org.junit.runner.JUnitCore \
-      $class || result=$(( $result + 1 ))
-done
-
-exit $result
diff --git a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java b/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java
deleted file mode 100644
index 2c5949c..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/ArrayMapTest.java
+++ /dev/null
@@ -1,472 +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.hoststubgen.frameworktest;
-
-// [ravewnwood] Copied from cts/, and commented out unsupported stuff.
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.util.ArrayMap;
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.AbstractMap;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import java.util.function.BiFunction;
-
-/**
- * Some basic tests for {@link android.util.ArrayMap}.
- */
-public class ArrayMapTest {
-    static final boolean DEBUG = false;
-
-    private static boolean compare(Object v1, Object v2) {
-        if (v1 == null) {
-            return v2 == null;
-        }
-        if (v2 == null) {
-            return false;
-        }
-        return v1.equals(v2);
-    }
-
-    private static void compareMaps(HashMap map, ArrayMap array) {
-        if (map.size() != array.size()) {
-            fail("Bad size: expected " + map.size() + ", got " + array.size());
-        }
-
-        Set<Entry> mapSet = map.entrySet();
-        for (Map.Entry entry : mapSet) {
-            Object expValue = entry.getValue();
-            Object gotValue = array.get(entry.getKey());
-            if (!compare(expValue, gotValue)) {
-                fail("Bad value: expected " + expValue + ", got " + gotValue
-                        + " at key " + entry.getKey());
-            }
-        }
-
-        for (int i = 0; i < array.size(); i++) {
-            Object gotValue = array.valueAt(i);
-            Object key = array.keyAt(i);
-            Object expValue = map.get(key);
-            if (!compare(expValue, gotValue)) {
-                fail("Bad value: expected " + expValue + ", got " + gotValue
-                        + " at key " + key);
-            }
-        }
-
-        if (map.entrySet().hashCode() != array.entrySet().hashCode()) {
-            fail("Entry set hash codes differ: map=0x"
-                    + Integer.toHexString(map.entrySet().hashCode()) + " array=0x"
-                    + Integer.toHexString(array.entrySet().hashCode()));
-        }
-
-        if (!map.entrySet().equals(array.entrySet())) {
-            fail("Failed calling equals on map entry set against array set");
-        }
-
-        if (!array.entrySet().equals(map.entrySet())) {
-            fail("Failed calling equals on array entry set against map set");
-        }
-
-        if (map.keySet().hashCode() != array.keySet().hashCode()) {
-            fail("Key set hash codes differ: map=0x"
-                    + Integer.toHexString(map.keySet().hashCode()) + " array=0x"
-                    + Integer.toHexString(array.keySet().hashCode()));
-        }
-
-        if (!map.keySet().equals(array.keySet())) {
-            fail("Failed calling equals on map key set against array set");
-        }
-
-        if (!array.keySet().equals(map.keySet())) {
-            fail("Failed calling equals on array key set against map set");
-        }
-
-        if (!map.keySet().containsAll(array.keySet())) {
-            fail("Failed map key set contains all of array key set");
-        }
-
-        if (!array.keySet().containsAll(map.keySet())) {
-            fail("Failed array key set contains all of map key set");
-        }
-
-        if (!array.containsAll(map.keySet())) {
-            fail("Failed array contains all of map key set");
-        }
-
-        if (!map.entrySet().containsAll(array.entrySet())) {
-            fail("Failed map entry set contains all of array entry set");
-        }
-
-        if (!array.entrySet().containsAll(map.entrySet())) {
-            fail("Failed array entry set contains all of map entry set");
-        }
-    }
-
-    private static void validateArrayMap(ArrayMap array) {
-        Set<Map.Entry> entrySet = array.entrySet();
-        int index = 0;
-        Iterator<Entry> entryIt = entrySet.iterator();
-        while (entryIt.hasNext()) {
-            Map.Entry entry = entryIt.next();
-            Object value = entry.getKey();
-            Object realValue = array.keyAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map entry set: expected key " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            value = entry.getValue();
-            realValue = array.valueAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map entry set: expected value " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            index++;
-        }
-
-        index = 0;
-        Set keySet = array.keySet();
-        Iterator keyIt = keySet.iterator();
-        while (keyIt.hasNext()) {
-            Object value = keyIt.next();
-            Object realValue = array.keyAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map key set: expected key " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            index++;
-        }
-
-        index = 0;
-        Collection valueCol = array.values();
-        Iterator valueIt = valueCol.iterator();
-        while (valueIt.hasNext()) {
-            Object value = valueIt.next();
-            Object realValue = array.valueAt(index);
-            if (!compare(realValue, value)) {
-                fail("Bad array map value col: expected value " + realValue
-                        + ", got " + value + " at index " + index);
-            }
-            index++;
-        }
-    }
-
-    private static void dump(Map map, ArrayMap array) {
-        Log.e("test", "HashMap of " + map.size() + " entries:");
-        Set<Map.Entry> mapSet = map.entrySet();
-        for (Map.Entry entry : mapSet) {
-            Log.e("test", "    " + entry.getKey() + " -> " + entry.getValue());
-        }
-        Log.e("test", "ArrayMap of " + array.size() + " entries:");
-        for (int i = 0; i < array.size(); i++) {
-            Log.e("test", "    " + array.keyAt(i) + " -> " + array.valueAt(i));
-        }
-    }
-
-    private static void dump(ArrayMap map1, ArrayMap map2) {
-        Log.e("test", "ArrayMap of " + map1.size() + " entries:");
-        for (int i = 0; i < map1.size(); i++) {
-            Log.e("test", "    " + map1.keyAt(i) + " -> " + map1.valueAt(i));
-        }
-        Log.e("test", "ArrayMap of " + map2.size() + " entries:");
-        for (int i = 0; i < map2.size(); i++) {
-            Log.e("test", "    " + map2.keyAt(i) + " -> " + map2.valueAt(i));
-        }
-    }
-
-    @Test
-    public void testCopyArrayMap() {
-        // map copy constructor test
-        ArrayMap newMap = new ArrayMap<Integer, String>();
-        for (int i = 0; i < 10; ++i) {
-            newMap.put(i, String.valueOf(i));
-        }
-        ArrayMap mapCopy = new ArrayMap(newMap);
-        if (!compare(mapCopy, newMap)) {
-            String msg = "ArrayMap copy constructor failure: expected " +
-                    newMap + ", got " + mapCopy;
-            Log.e("test", msg);
-            dump(newMap, mapCopy);
-            fail(msg);
-            return;
-        }
-    }
-
-    @Test
-    public void testEqualsArrayMap() {
-        ArrayMap<Integer, String> map1 = new ArrayMap<>();
-        ArrayMap<Integer, String> map2 = new ArrayMap<>();
-        HashMap<Integer, String> map3 = new HashMap<>();
-        if (!compare(map1, map2) || !compare(map1, map3) || !compare(map3, map2)) {
-            fail("ArrayMap equals failure for empty maps " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-
-        for (int i = 0; i < 10; ++i) {
-            String value = String.valueOf(i);
-            map1.put(i, value);
-            map2.put(i, value);
-            map3.put(i, value);
-        }
-        if (!compare(map1, map2) || !compare(map1, map3) || !compare(map3, map2)) {
-            fail("ArrayMap equals failure for populated maps " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-
-        map1.remove(0);
-        if (compare(map1, map2) || compare(map1, map3) || compare(map3, map1)) {
-            fail("ArrayMap equals failure for map size " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-
-        map1.put(0, "-1");
-        if (compare(map1, map2) || compare(map1, map3) || compare(map3, map1)) {
-            fail("ArrayMap equals failure for map contents " + map1 + ", " +
-                    map2 + ", " + map3);
-        }
-    }
-
-    private static void checkEntrySetToArray(ArrayMap<?, ?> testMap) {
-        try {
-            testMap.entrySet().toArray();
-            fail();
-        } catch (UnsupportedOperationException expected) {
-        }
-
-        try {
-            Map.Entry<?, ?>[] entries = new Map.Entry[20];
-            testMap.entrySet().toArray(entries);
-            fail();
-        } catch (UnsupportedOperationException expected) {
-        }
-    }
-
-    // http://b/32294038, Test ArrayMap.entrySet().toArray()
-    @Test
-    public void testEntrySetArray() {
-        // Create
-        ArrayMap<Integer, String> testMap = new ArrayMap<>();
-
-        // Test empty
-        checkEntrySetToArray(testMap);
-
-        // Test non-empty
-        for (int i = 0; i < 10; ++i) {
-            testMap.put(i, String.valueOf(i));
-        }
-        checkEntrySetToArray(testMap);
-    }
-
-    @Test
-    public void testCanNotIteratePastEnd_entrySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Set<Map.Entry<String, String>> expectedEntriesToIterate = new HashSet<>(Arrays.asList(
-                entryOf("key 1", "value 1"),
-                entryOf("key 2", "value 2")
-        ));
-        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
-
-        // Assert iteration over the expected two entries in any order
-        assertTrue(iterator.hasNext());
-        Map.Entry<String, String> firstEntry = copyOf(iterator.next());
-        assertTrue(expectedEntriesToIterate.remove(firstEntry));
-
-        assertTrue(iterator.hasNext());
-        Map.Entry<String, String> secondEntry = copyOf(iterator.next());
-        assertTrue(expectedEntriesToIterate.remove(secondEntry));
-
-        assertFalse(iterator.hasNext());
-
-        try {
-            iterator.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
-    }
-
-    private static <K, V> Map.Entry<K, V> entryOf(K key, V value) {
-        return new AbstractMap.SimpleEntry<>(key, value);
-    }
-
-    private static <K, V> Map.Entry<K, V> copyOf(Map.Entry<K, V> entry) {
-        return entryOf(entry.getKey(), entry.getValue());
-    }
-
-    @Test
-    public void testCanNotIteratePastEnd_keySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Set<String> expectedKeysToIterate = new HashSet<>(Arrays.asList("key 1", "key 2"));
-        Iterator<String> iterator = map.keySet().iterator();
-
-        // Assert iteration over the expected two keys in any order
-        assertTrue(iterator.hasNext());
-        String firstKey = iterator.next();
-        assertTrue(expectedKeysToIterate.remove(firstKey));
-
-        assertTrue(iterator.hasNext());
-        String secondKey = iterator.next();
-        assertTrue(expectedKeysToIterate.remove(secondKey));
-
-        assertFalse(iterator.hasNext());
-
-        try {
-            iterator.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
-    }
-
-    @Test
-    public void testCanNotIteratePastEnd_valuesIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Set<String> expectedValuesToIterate = new HashSet<>(Arrays.asList("value 1", "value 2"));
-        Iterator<String> iterator = map.values().iterator();
-
-        // Assert iteration over the expected two values in any order
-        assertTrue(iterator.hasNext());
-        String firstValue = iterator.next();
-        assertTrue(expectedValuesToIterate.remove(firstValue));
-
-        assertTrue(iterator.hasNext());
-        String secondValue = iterator.next();
-        assertTrue(expectedValuesToIterate.remove(secondValue));
-
-        assertFalse(iterator.hasNext());
-
-        try {
-            iterator.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
-    }
-
-    @Test
-    public void testForEach() {
-        ArrayMap<String, Integer> map = new ArrayMap<>();
-
-        for (int i = 0; i < 50; ++i) {
-            map.put(Integer.toString(i), i * 10);
-        }
-
-        // Make sure forEach goes through all of the elements.
-        HashMap<String, Integer> seen = new HashMap<>();
-        map.forEach(seen::put);
-        compareMaps(seen, map);
-    }
-
-    /**
-     * The entrySet Iterator returns itself from each call to {@code next()}. This is unusual
-     * behavior for {@link Iterator#next()}; this test ensures that any future change to this
-     * behavior is deliberate.
-     */
-    @Test
-    public void testUnusualBehavior_eachEntryIsSameAsIterator_entrySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
-
-        assertSame(iterator, iterator.next());
-        assertSame(iterator, iterator.next());
-    }
-
-    @SuppressWarnings("SelfEquals")
-    @Test
-    public void testUnusualBehavior_equalsThrowsAfterRemove_entrySetIterator() {
-        Map<String, String> map = new ArrayMap<>();
-        map.put("key 1", "value 1");
-        map.put("key 2", "value 2");
-        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
-        iterator.next();
-        iterator.remove();
-        try {
-            iterator.equals(iterator);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    private static <T> void assertEqualsBothWays(T a, T b) {
-        assertEquals(a, b);
-        assertEquals(b, a);
-        assertEquals(a.hashCode(), b.hashCode());
-    }
-
-    @Test
-    public void testRemoveAll() {
-        final ArrayMap<Integer, String> map = new ArrayMap<>();
-        for (Integer i : Arrays.asList(0, 1, 2, 3, 4, 5)) {
-            map.put(i, i.toString());
-        }
-
-        final ArrayMap<Integer, String> expectedMap = new ArrayMap<>();
-        for (Integer i : Arrays.asList(2, 4)) {
-            expectedMap.put(i, String.valueOf(i));
-        }
-        map.removeAll(Arrays.asList(0, 1, 3, 5, 6));
-        if (!compare(map, expectedMap)) {
-            fail("ArrayMap removeAll failure, expect " + expectedMap + ", but " + map);
-        }
-
-        map.removeAll(Collections.emptyList());
-        if (!compare(map, expectedMap)) {
-            fail("ArrayMap removeAll failure for empty maps, expect " + expectedMap + ", but " +
-                    map);
-        }
-
-        map.removeAll(Arrays.asList(2, 4));
-        if (!map.isEmpty()) {
-            fail("ArrayMap removeAll failure, expect empty, but " + map);
-        }
-    }
-
-    @Test
-    public void testReplaceAll() {
-        final ArrayMap<Integer, Integer> map = new ArrayMap<>();
-        final ArrayMap<Integer, Integer> expectedMap = new ArrayMap<>();
-        final BiFunction<Integer, Integer, Integer> function = (k, v) -> 2 * v;
-        for (Integer i : Arrays.asList(0, 1, 2, 3, 4, 5)) {
-            map.put(i, i);
-            expectedMap.put(i, 2 * i);
-        }
-
-        map.replaceAll(function);
-        if (!compare(map, expectedMap)) {
-            fail("ArrayMap replaceAll failure, expect " + expectedMap + ", but " + map);
-        }
-    }
-}
diff --git a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java b/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java
deleted file mode 100644
index 3e33b54..0000000
--- a/tools/hoststubgen/hoststubgen/test-framework/src/com/android/hoststubgen/frameworktest/LogTest.java
+++ /dev/null
@@ -1,41 +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.hoststubgen.frameworktest;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-/**
- * Some basic tests for {@link android.util.Log}.
- */
-public class LogTest {
-    @Test
-    public void testBasicLogging() {
-        Log.v("TAG", "Test v log");
-        Log.d("TAG", "Test d log");
-        Log.i("TAG", "Test i log");
-        Log.w("TAG", "Test w log");
-        Log.e("TAG", "Test e log");
-    }
-
-    @Test
-    public void testNativeMethods() {
-        assertThat(Log.isLoggable("mytag", Log.INFO)).isTrue();
-    }
-}
diff --git a/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh b/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh
deleted file mode 100755
index 7268123..0000000
--- a/tools/hoststubgen/scripts/build-framework-hostside-jars-and-extract.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/bin/bash
-# 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.
-
-
-# Script to build `framework-host-stub` and `framework-host-impl`, and copy the
-# generated jars to $out, and unzip them. Useful for looking into the generated files.
-
-source "${0%/*}"/../common.sh
-
-out=framework-all-stub-out
-
-rm -fr $out
-mkdir -p $out
-
-# Build the jars with `m`.
-run m framework-all-hidden-api-host
-
-# Copy the jar to out/ and extract them.
-run cp \
-    $SOONG_INT/frameworks/base/tools/hoststubgen/hoststubgen/framework-all-hidden-api-host/linux_glibc_common/gen/* \
-    $out
-
-extract $out/*.jar
diff --git a/tools/hoststubgen/scripts/run-all-tests.sh b/tools/hoststubgen/scripts/run-all-tests.sh
index 222c874..a6847ae 100755
--- a/tools/hoststubgen/scripts/run-all-tests.sh
+++ b/tools/hoststubgen/scripts/run-all-tests.sh
@@ -22,7 +22,6 @@
 
 # These tests are known to pass.
 READY_TEST_MODULES=(
-  HostStubGenTest-framework-all-test-host-test
   hoststubgen-test-tiny-test
   CtsUtilTestCasesRavenwood
   CtsOsTestCasesRavenwood # This one uses native sustitution, so let's run it too.
@@ -30,7 +29,6 @@
 
 MUST_BUILD_MODULES=(
     "${NOT_READY_TEST_MODULES[*]}"
-    HostStubGenTest-framework-test
 )
 
 # First, build all the test / etc modules. This shouldn't fail.
@@ -44,11 +42,8 @@
 # files, and they may fail when something changes in the build system.
 run ./hoststubgen/test-tiny-framework/diff-and-update-golden.sh
 
-run ./hoststubgen/test-framework/run-test-without-atest.sh
-
 run ./hoststubgen/test-tiny-framework/run-test-manually.sh
 run atest $ATEST_ARGS tiny-framework-dump-test
-run ./scripts/build-framework-hostside-jars-and-extract.sh
 
 # This script is already broken on goog/master
 # run ./scripts/build-framework-hostside-jars-without-genrules.sh